在本教程中,我將演示如何使用 Node 作為後端和 htmx 作為前端來構建功能齊全的 CRUD 應用程序。這將演示 htmx 如何集成到全棧應用程序中,使您能夠評估其有效性並確定它是否是您未來項目的不錯選擇。
htmx 是一個現代 JavaScript 庫,旨在通過實現部分 HTML 更新來增強Web應用,而無需重新加載整個頁面。與傳統前端框架中的 JSON 有效載荷不同,它通過有線方式發送 HTML 來實現這一功能。
我們將要構建什麼
我們將開發一個簡單的聯繫人管理器,能夠執行所有 CRUD 操作:創建、讀取、更新和刪除聯繫人。通過利用 htmx,該應用程序將提供單頁應用程序 (SPA) 的感覺,從而增強交互性和用户體驗。
如果用户禁用 JavaScript,應用程序將以整頁刷新的方式運行,從而保持可用性和可發現性。這種方法展示了 htmx 創建現代 Web 應用程序的能力,同時保持它們的可訪問性和 SEO 友好性。
這就是我們最終得到的結果。
本文的代碼可以在隨附的 GitHub 存儲庫中找到。
先決條件
要學習本教程,您需要在 PC 上安裝 Node.js。如果您尚未安裝 Node,請前往官方 Node 下載頁面並獲取適合您系統的正確二進制文件。或者,您可能想使用版本管理器安裝 Node。這種方法允許您安裝多個 Node 版本並在它們之間隨意切換。
除此之外,熟悉 Node、Pug(我們將使用它們作為模板引擎)和 htmx 會有所幫助,但不是必需的。如果您想複習以上任何內容,請查看我們的教程:使用 Node 構建簡單的初學者應用程序、Pug HTML 模板預處理器指南和 htmx 簡介。
在開始之前,請運行以下命令:
node -v
npm -v
您應該看到如下輸出:
v20.11.1
10.4.0
這確認了 Node 和 npm 已安裝在您的計算機上,並且可以從命令行環境進行訪問。
設置項目
讓我們從搭建一個新的 Node 項目開始:
mkdir contact-manager
cd contact-manager
npm init -y
這應該在項目根目錄中創建一個 package.json 文件。
接下來,讓我們安裝我們需要的依賴項:
npm i express method-override pug
在這些包中,Express 是我們應用程序的支柱。它是一個快速且簡約的 Web 框架,提供了一種簡單的方法來處理請求和響應,並將 URL 路由到特定的處理函數。 Pug 將充當我們的模板引擎,而我們將使用方法覆蓋在客户端不支持的地方使用 HTTP 動詞,例如 PUT 和 DELETE。
接下來,在根目錄中創建一個 app.js 文件:
touch app.js
並添加以下內容:
const express = require('express');
const path = require('path');
const routes = require('./routes/index');
const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(express.static('public'));
app.use('/', routes);
const server = app.listen(3000, () => {
console.log(`Express is running on port ${server.address().port}`);
});
在這裏,我們正在設置 Express 應用程序的結構。這包括將 Pug 配置為渲染視圖的視圖引擎、定義靜態資產的目錄以及連接路由器。
該應用程序偵聽端口 3000,並使用控制枱日誌來確認 Express 正在運行並準備好處理指定端口上的請求。此設置構成了我們應用程序的基礎,並準備好通過更多功能和路由進行擴展。
接下來,讓我們創建路由文件:
mkdir routes
touch routes/index.js
打開該文件並添加以下內容:
const express = require('express');
const router = express.Router();
// GET /contacts
router.get('/contacts', async (req, res) => {
res.send('It works!');
});
在這裏,我們在新創建的路由目錄中設置基本路由。此路由在 /contacts 端點偵聽 GET 請求,並使用簡單的確認消息進行響應,表明一切正常。
接下來,使用以下內容更新 package.json 文件的“scripts”部分:
"scripts": {
"dev": "node --watch app.js"
},
這利用了 Node.js 中的新監視模式,只要檢測到任何更改,該模式就會重新啓動我們的應用程序。
最後,使用 npm run dev 啓動所有內容,然後在瀏覽器中訪問 http://localhost:3000/contacts/。您應該會看到一條消息“It works!”。
激動人心的時刻!
顯示所有聯繫人
現在讓我們添加一些要顯示的聯繫人。由於我們專注於 htmx,因此為了簡單起見,我們將使用硬編碼數組。這將使事情變得精簡,使我們能夠專注於 htmx 的動態功能,而無需複雜的數據庫集成。
對於那些有興趣稍後添加數據庫的人來説,SQLite 和 Sequelize 是不錯的選擇,它們提供了不需要單獨數據庫服務器的基於文件的系統。
話雖如此,請將以下內容添加到第一個路由之前的 index.js 中:
const contacts = [
{ id: 1, name: 'John Doe', email: 'john.doe@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane.smith@example.com' },
{ id: 3, name: 'Emily Johnson', email: 'emily.johnson@example.com' },
{ id: 4, name: 'Aarav Patel', email: 'aarav.patel@example.com' },
{ id: 5, name: 'Liu Wei', email: 'liu.wei@example.com' },
{ id: 6, name: 'Fatima Zahra', email: 'fatima.zahra@example.com' },
{ id: 7, name: 'Carlos Hernández', email: 'carlos.hernandez@example.com' },
{ id: 8, name: 'Olivia Kim', email: 'olivia.kim@example.com' },
{ id: 9, name: 'Kwame Nkrumah', email: 'kwame.nkrumah@example.com' },
{ id: 10, name: 'Chen Yu', email: 'chen.yu@example.com' },
];
現在,我們需要為路由創建一個顯示模板。創建一個包含 index.pug 文件的 views 文件夾:
mkdir views
touch views/index.pug
並添加以下內容:
doctype html
html
head
meta(charset='UTF-8')
title Contact Manager
link(rel='preconnect', href='https://fonts.googleapis.com')
link(rel='preconnect', href='https://fonts.gstatic.com', crossorigin)
link(href='https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap', rel='stylesheet')
link(rel='stylesheet', href='/styles.css')
body
header
a(href='/contacts')
h1 Contact Manager
section#sidebar
ul.contact-list
each contact in contacts
li #{contact.name}
div.actions
a(href='/contacts/new') New Contact
main#content
p Select a contact
script(src='https://unpkg.com/htmx.org@1.9.10')
在此模板中,我們為應用程序佈置 HTML 結構。在 head 部分,我們包含了來自 Google Fonts 的 Roboto 字體和自定義樣式的樣式表。
正文分為標題、用於列出聯繫人的側邊欄以及用於存放所有聯繫信息的主要內容區域。內容區域當前包含一個佔位符。在正文的末尾,我們還包含來自 CDN 的最新版本的 htmx 庫。
該模板期望接收一個聯繫人數組(在 contacts 變量中),我們在側邊欄中對其進行迭代,並使用 Pug 的插值語法在無序列表中輸出每個聯繫人姓名。
接下來,讓我們創建自定義樣式表:
mkdir public
touch public/styles.css
我不想在這裏列出樣式。請從隨附的 GitHub 存儲庫中的 CSS 文件中複製它們,或者隨意添加一些您自己的 CSS 文件。 🙂
回到 index.js,更新路由以使用模板:
// GET /contacts
router.get('/contacts', (req, res) => {
res.render('index', { contacts });
});
現在,當您刷新頁面時,您應該會看到類似這樣的內容。
顯示單個聯繫人
到目前為止,我們所做的只是建立了一個基本的 Express 應用程序。讓我們改變一下,最後添加 htmx。下一步要做的是,當用户點擊側邊欄中的聯繫人時,該聯繫人的信息就會顯示在主內容區域--自然不需要重新載入整個頁面。
首先,讓我們將側邊欄移至其自己的模板中:
touch views/sidebar.pug
將以下內容添加到這個新文件中:
ul.contact-list
each contact in contacts
li
a(
href=`/contacts/${contact.id}`,
hx-get=`/contacts/${contact.id}`,
hx-target='#content',
hx-push-url='true'
)= contact.name
div.actions
a(href='/contacts/new') New Contact
這裏我們為每個聯繫人創建了一個指向 /contacts/${contact.id} 的鏈接,並添加了三個 htmx 屬性:
hx-get:當用户單擊鏈接時,htmx 將攔截單擊並通過 Ajax 向/contacts/${contact.id}端點發出 GET 請求。hx-target:當請求完成時,響應將被插入到 ID 為content的 div 中。我們在這裏沒有指定任何類型的交換策略,因此 div 的內容將被 Ajax 請求返回的內容替換。這是默認行為。hx-push-url:這將確保htx-get中指定的值被推送到瀏覽器的歷史堆棧中,從而更改 URL。
更新 index.pug 以使用我們的模板:
section#sidebar
include sidebar.pug
請記住:Pug 對空格敏感,因此請務必使用正確的縮進。
現在讓我們在 index.js 中創建一個新端點以返回 htmx 期望的 HTML 響應:
// GET /contacts/1
router.get('/contacts/:id', (req, res) => {
const { id } = req.params;
const contact = contacts.find((c) => c.id === Number(id));
res.send(`
<h2>${contact.name}</h2>
<p><strong>Name:</strong> ${contact.name}</p>
<p><strong>Email:</strong> ${contact.email}</p>
`);
});
如果保存並刷新瀏覽器,您現在應該能夠查看每個聯繫人的詳細信息。
網絡上的 HTML
讓我們花點時間瞭解一下這裏發生了什麼。正如文章開頭提到的,htmx 通過網絡傳輸 HTML,而不是傳統前端框架的 JSON 有效負載。
如果我們打開瀏覽器的開發人員工具,切換到“Network”選項卡並單擊其中一個聯繫人,我們就可以看到這一點。收到來自前端的請求後,我們的 Express 應用程序會生成顯示該聯繫人所需的 HTML,並將其發送到瀏覽器,其中 htmx 將其交換到 UI 中的正確位置。
處理全頁刷新
所以事情進展得很順利,是吧?感謝 htmx,我們通過在錨標記上指定幾個屬性來使頁面動態化。不幸的是,有一個問題……
如果您顯示聯繫人,然後刷新頁面,我們可愛的用户界面就會消失,您看到的只是裸露的聯繫方式詳細信息。如果您直接在瀏覽器中加載 URL,也會發生同樣的情況。
如果你仔細想想,其原因是顯而易見的。當您訪問 http://localhost:3000/contacts/1 之類的 URL 時, '/contacts/:id' 的 Express 路由將啓動並返回聯繫人的 HTML,正如我們告訴它的那樣。它對我們用户界面的其餘部分一無所知。
為了解決這個問題,我們需要做一些改動。在服務器上,我們需要檢查是否存在 HX-Request 標頭,它表明請求來自 htmx。如果存在,我們就可以發送部分內容。否則,我們需要發送整個頁面。
像這樣更改路由處理程序:
// GET /contacts/1
router.get('/contacts/:id', (req, res) => {
const { id } = req.params;
const contact = contacts.find((c) => c.id === Number(id));
if (req.headers['hx-request']) {
res.send(`
<h2>${contact.name}</h2>
<p><strong>Name:</strong> ${contact.name}</p>
<p><strong>Email:</strong> ${contact.email}</p>
`);
} else {
res.render('index', { contacts });
}
});
現在,當您重新加載頁面時,用户界面不會消失。但是,它確實會從您正在查看的任何聯繫人恢復為消息“選擇聯繫人”,這並不理想。
為了解決這個問題,我們可以在 index.pug 模板中引入 case 語句:
main#content
case action
when 'show'
h2 #{contact.name}
p #[strong Name:] #{contact.name}
p #[strong Email:] #{contact.email}
when 'new'
// Coming soon
when 'edit'
// Coming soon
default
p Select a contact
最後更新路由處理程序:
if (req.headers['hx-request']) {
// As before
} else {
res.render('index', { action: 'show', contacts, contact });
}
請注意,我們現在傳入一個 contact 變量,該變量將在整個頁面重新加載時使用。
這樣,我們的應用程序應該能夠承受刷新或直接加載聯繫人。
快速重構
雖然這樣做可行,但您可能會注意到,我們的路由處理程序和主 pug 模板中都有一些重複的內容。這種情況並不理想,只要聯繫人的屬性不超過幾個,或者我們需要使用一些邏輯來決定顯示哪些屬性,事情就會開始變得臃腫。
為了解決這個問題,讓我們將聯繫人移動到自己的模板中:
touch views/contact.pug
在新創建的模板中,添加以下內容:
h2 #{contact.name}
p #[strong Name:] #{contact.name}
p #[strong Email:] #{contact.email}
在主模板( index.pug )中:
main#content
case action
when 'show'
include contact.pug
還有我們的路由處理程序:
if (req.headers['hx-request']) {
res.render('contact', { contact });
} else {
res.render('index', { action: 'show', contacts, contact });
}
事情應該仍然像以前一樣工作,但現在我們已經刪除了重複的代碼。
新的聯繫表
我們要關注的下一個任務是創建新聯繫人。本教程的這一部分將指導您設置表單和後端邏輯,使用 htmx 動態處理提交。
讓我們從更新側邊欄模板開始。更改:
div.actions
a(href='/contacts/new') New Contact
… 到:
div.actions
a(
href='/contacts/new',
hx-get='/contacts/new',
hx-target='#content',
hx-push-url='true'
) New Contact
這將使用與鏈接相同的 htmx 屬性來顯示聯繫人:hx-get 將通過 Ajax 向 /contacts/new 端點發出 GET 請求,hx-target 將指定插入響應的位置,hx-push-url 將確保更改 URL。
現在讓我們為表單創建一個新模板:
touch views/form.pug
並添加以下代碼:
h2 New Contact
form(
action='/contacts',
method='POST',
hx-post='/contacts',
hx-target='#sidebar',
hx-on::after-request='if(event.detail.successful) this.reset()'
)
label(for='name') Name:
input#name(type='text', name='name', required)
label(for='email') Email:
input#email(type='email', name='email', required)
div.actions
button(type='submit') Submit
在這裏,我們使用 hx-post 屬性告訴 htmx 攔截表單提交,並向 /contacts 端點發出帶有表單數據的 POST 請求。結果(更新的聯繫人列表)將被插入到側邊欄中。在這種情況下,我們不想更改 URL,因為用户可能想要輸入多個新聯繫人。但是,我們確實希望在成功提交後清空表單,這就是 hx-on::after-request 的作用。 hx-on* 屬性允許您內聯嵌入腳本以直接響應元素上的事件。你可以在這裏讀更多關於它的內容。
接下來,我們在 index.js 中添加表單的路由:
// GET /contacts
...
// GET /contacts/new
router.get('/contacts/new', (req, res) => {
if (req.headers['hx-request']) {
res.render('form');
} else {
res.render('index', { action: 'new', contacts, contact: {} });
}
});
// GET /contacts/1
...
路由順序在這裏很重要。如果您先有 '/contacts/:id' 路由,那麼 Express 將嘗試查找 ID 為 new 的聯繫人。
最後,更新我們的 index.pug 模板以使用以下形式:
when 'new'
include form.pug
刷新頁面,此時您應該能夠通過單擊側欄中的“New Contact”鏈接來呈現新的聯繫人表單。
創建聯繫人
現在我們需要創建一個路由來處理表單提交。
首先更新 app.js 以使我們能夠訪問路由處理程序中的表單數據。
const express = require('express');
const path = require('path');
const routes = require('./routes/index');
const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
+ app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/', routes);
const server = app.listen(3000, () => {
console.log(`Express is running on port ${server.address().port}`);
});
以前,我們會使用 body-parser 包,但我最近了解到這不再是必要的。
然後將以下內容添加到 index.js :
// POST /contacts
router.post('/contacts', (req, res) => {
const newContact = {
id: contacts.length + 1,
name: req.body.name,
email: req.body.email,
};
contacts.push(newContact);
if (req.headers['hx-request']) {
res.render('sidebar', { contacts });
} else {
res.render('index', { action: 'new', contacts, contact: {} });
}
});
在這裏,我們使用從客户端收到的數據創建一個新聯繫人,並將其添加到 contacts 數組中。然後,我們重新渲染側邊欄,並向其傳遞更新的聯繫人列表。
請注意,如果您正在製作任何類型的有用户的應用程序,則由您負責驗證從客户端接收的數據。在我們的示例中,我添加了一些基本的客户端驗證,但這很容易被繞過。
我上面鏈接的 Node 教程中有一個示例,説明如何使用 express-validator 包來驗證服務器上的輸入。
現在,如果您刷新瀏覽器並嘗試添加聯繫人,它應該按預期工作:新聯繫人應添加到側邊欄,並且應重置表單。
添加 toast 消息提示
這很好,但現在我們需要一種方法來通知用户聯繫人已添加。在典型的應用程序中,我們會使用 toast 消息——一種臨時通知,提醒用户操作的結果。
我們使用 htmx 遇到的問題是,我們在成功創建新聯繫人後更新側邊欄,但這不是我們希望顯示 toast 消息的位置。更好的位置將位於新聯繫表格上方。
為了解決這個問題,我們可以使用 hx-swap-oob 屬性。這允許您指定響應中的某些內容應交換到目標以外的 DOM 中,即“Out of Band”。
更新路由處理程序如下:
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
const html = `
<main id="content" hx-swap-oob="afterbegin">
<p class="flash">Contact was successfully added!</p>
</main>
${sidebarHtml}
`;
res.send(html);
});
} else {
res.render('index', { action: 'new', contacts, contact: {} });
}
在這裏,我們像以前一樣渲染側邊欄,但向 render 方法傳遞一個匿名函數作為第三個參數。該函數接收通過調用 res.render('sidebar', { contacts }) 生成的 HTML,然後我們可以使用它來組裝最終響應。
通過指定交換策略 "afterbegin" ,將 toast 消息插入到容器的頂部。
現在,當我們添加聯繫人時,我們應該會收到一條不錯的消息,告訴我們發生了什麼。
編輯聯繫人
為了更新聯繫人,我們將重用上一節中創建的表單。
讓我們首先更新 contact.pug 模板以添加以下內容:
div.actions
a(
href=`/contacts/${contact.id}/edit`,
hx-get=`/contacts/${contact.id}/edit`,
hx-target='#content',
hx-push-url='true'
) Edit Contact
這將在聯繫人詳細信息下方添加一個編輯聯繫人按鈕。正如我們之前所見,當單擊鏈接時, hx-get 將通過 Ajax 向 /${contact.id}/edit 端點發出 GET 請求, hx-target 將指定插入位置響應, hx-push-url 將確保 URL 發生更改。
現在讓我們更改 index.pug 模板以使用以下形式:
when 'edit'
include form.pug
還添加一個路由處理程序來顯示錶單:
// GET /contacts/1/edit
router.get('/contacts/:id/edit', (req, res) => {
const { id } = req.params;
const contact = contacts.find((c) => c.id === Number(id));
if (req.headers['hx-request']) {
res.render('form', { contact });
} else {
res.render('index', { action: 'edit', contacts, contact });
}
});
請注意,我們使用請求中的 ID 檢索聯繫人,然後將該聯繫人傳遞到表單。
我們還需要更新新的聯繫人處理程序以執行相同的操作,但此處傳遞一個空對象:
// GET /contacts/new
router.get('/contacts/new', (req, res) => {
if (req.headers['hx-request']) {
- res.render('form');
+ res.render('form', { contact: {} });
} else {
res.render('index', { action: 'new', contacts, contact: {} });
}
});
然後我們需要更新表單本身:
- isEditing = () => !(Object.keys(contact).length === 0);
h2=isEditing() ? "Edit Contact" : "New Contact"
form(
action=isEditing() ? `/update/${contact.id}?_method=PUT` : '/contacts',
method='POST',
hx-post=isEditing() ? false : '/contacts',
hx-put=isEditing() ? `/update/${contact.id}` : false,
hx-target='#sidebar',
hx-push-url=isEditing() ? `/contacts/${contact.id}` : false
hx-on::after-request='if(event.detail.successful) this.reset()',
)
label(for='name') Name:
input#name(type='text', name='name', required, value=contact.name)
label(for='email') Email:
input#email(type='email', name='email', required, value=contact.email)
div.actions
button(type='submit') Submit
當我們向此表單傳遞聯繫人或空對象時,我們現在有一種簡單的方法來確定我們是否處於“編輯”或“創建”模式。我們可以通過檢查 Object.keys(contact).length 來做到這一點。我們還可以使用 Pug 的無緩衝代碼語法將此檢查提取到文件頂部的一個小輔助函數中。
一旦我們知道自己所處的模式,我們就可以有條件地更改頁面標題,然後決定向表單標記添加哪些屬性。對於編輯表單,我們需要添加 hx-put 屬性並將其設置為 /update/${contact.id} 。保存聯繫人詳細信息後,我們還需要更新 URL。
為了做到這一切,我們可以利用這樣一個事實:如果條件返回 false ,Pug 將從標籤中省略該屬性。
這意味着:
form(
action=isEditing() ? `/update/${contact.id}?_method=PUT` : '/contacts',
method='POST',
hx-post=isEditing() ? false : '/contacts',
hx-put=isEditing() ? `/update/${contact.id}` : false,
hx-target='#sidebar',
hx-on::after-request='if(event.detail.successful) this.reset()',
hx-push-url=isEditing() ? `/contacts/${contact.id}` : false
)
當 isEditing() 返回 false 時將編譯為以下內容:
<form
action="/contacts"
method="POST"
hx-post="/contacts"
hx-target="#sidebar"
hx-on::after-request="if(event.detail.successful) this.reset()"
>
...
</form>
但是當 isEditing() 返回 true 時,它將編譯為:
<form
action="/update/1?_method=PUT"
method="POST"
hx-put="/update/1"
hx-target="#sidebar"
hx-on::after-request="if(event.detail.successful) this.reset()"
hx-push-url="/contacts/1"
>
...
</form>
在其更新狀態下,請注意表單操作是 "/update/1?_method=PUT" 。添加此查詢字符串參數是因為我們正在使用方法覆蓋包,它將使我們的路由器響應 PUT 請求。
開箱即用的 htmx 可以發送 PUT 和 DELETE 請求,但瀏覽器卻不行。這意味着,如果我們要處理 JavaScript 被禁用的情況,就需要複製我們的路由處理程序,讓它同時響應 PUT(htmx)和 POST(瀏覽器)。使用這種中間件將使我們的代碼保持 DRY。
讓我們繼續將其添加到 app.js :
const express = require('express');
const path = require('path');
+ const methodOverride = require('method-override');
const routes = require('./routes/index');
const app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
+ app.use(methodOverride('_method'));
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
app.use('/', routes);
const server = app.listen(3000, () => {
console.log(`Express is running on port ${server.address().port}`);
});
最後,讓我們用新的路由處理程序更新 index.js :
// PUT /contacts/1
router.put('/update/:id', (req, res) => {
const { id } = req.params;
const newContact = {
id: Number(id),
name: req.body.name,
email: req.body.email,
};
const index = contacts.findIndex((c) => c.id === Number(id));
if (index !== -1) contacts[index] = newContact;
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
res.render('contact', { contact: contacts[index] }, (err, contactHTML) => {
const html = `
${sidebarHtml}
<main id="content" hx-swap-oob="true">
<p class="flash">Contact was successfully updated!</p>
${contactHTML}
</main>
`;
res.send(html);
});
});
} else {
res.redirect(`/contacts/${index + 1}`);
}
});
希望現在沒有什麼太神秘的事情了。在處理程序的開頭,我們從請求參數中獲取聯繫人 ID。然後,我們找到想要更新的聯繫人,並將其替換為根據我們收到的表單數據創建的新聯繫人。
在處理 htmx 請求時,我們首先用更新的聯繫人列表呈現側邊欄模板。然後,我們用更新的聯繫人渲染聯繫人模板,並使用這兩次調用的結果來組合我們的響應。與之前一樣,我們使用 “Out of Band” 更新創建一條 toast 消息,告知用户聯繫人已更新。
此時,您應該能夠更新聯繫人。
刪除聯繫人
最後一個難題是刪除聯繫人的能力。讓我們在聯繫人模板中添加一個按鈕來執行此操作:
div.actions
form(method='POST', action=`/delete/${contact.id}?_method=DELETE`)
button(
type='submit',
hx-delete=`/delete/${contact.id}`,
hx-target='#sidebar',
hx-push-url='/contacts'
class='link'
) Delete Contact
a(
// as before
)
請注意,最好使用表單和按鈕來發出 DELETE 請求。表單是為導致更改(例如刪除)的操作而設計的,這確保了語義的正確性。此外,使用鏈接進行刪除操作可能存在風險,因為搜索引擎可能會無意中跟蹤鏈接,從而可能導致不必要的刪除。
話雖這麼説,我添加了一些 CSS 來將按鈕設置為鏈接樣式,因為按鈕很難看。如果您之前從存儲庫複製了樣式,那麼您的代碼中已經包含了該樣式。
最後,我們的路由處理程序在 index.js 中:
// DELETE /contacts/1
router.delete('/delete/:id', (req, res) => {
const { id } = req.params;
const index = contacts.findIndex((c) => c.id === Number(id));
if (index !== -1) contacts.splice(index, 1);
if (req.headers['hx-request']) {
res.render('sidebar', { contacts }, (err, sidebarHtml) => {
const html = `
<main id="content" hx-swap-oob="true">
<p class="flash">Contact was successfully deleted!</p>
</main>
${sidebarHtml}
`;
res.send(html);
});
} else {
res.redirect('/contacts');
}
});
刪除聯繫人後,我們將更新側邊欄並向用户顯示一條提示消息。
更進一步
就這樣吧。
在本文中,我們使用 Node 和 Express 作為後端,使用 htmx 作為前端,製作了一個全棧 CRUD 應用程序。在此過程中,我演示了 htmx 如何簡化向 Web 應用程序添加動態行為,減少對複雜 JavaScript 和整頁重新加載的需求,從而使用户體驗更流暢、更具交互性。
作為額外的好處,該應用程序無需 JavaScript 也能正常運行。
然而,雖然我們的應用程序功能齊全,但不可否認它還有些簡陋。如果您希望繼續探索 htmx,您可能希望考慮在應用程序狀態之間實現視圖轉換,或者向表單添加一些進一步的驗證 - 例如,驗證電子郵件地址是否來自特定域。
我在 htmx 簡介中提供了這兩件事(以及更多)的示例。