為什么服務(wù)器應(yīng)該再次處理網(wǎng)頁
有那么一刻,我們悄悄達(dá)成了共識(shí):服務(wù)器別再渲染頁面了。 “給我 JSON 就行,”我們說,“剩下交給前端。”
一開始確實(shí)挺順:我們?cè)炝?fetcher、hydrator、router、transition;我們優(yōu)化、記憶化、調(diào)和;把應(yīng)用拆成組件,再把組件拆成更小的組件,然后再抽成 hooks。我們自豪地把服務(wù)器降格成一個(gè)只會(huì)吐 JSON 的自動(dòng)販賣機(jī)。
可慢慢地,你會(huì)問:為什么?為什么要讓服務(wù)器忘了如何渲染一張表? 為什么要把那個(gè)最清楚一切(用戶、權(quán)限、業(yè)務(wù)規(guī)則、異常)的層,削成只出 JSON 的管道?
不妨聊聊:當(dāng)服務(wù)器把控制權(quán)拿回來,Web 會(huì)變成什么樣,我們又能得到什么。
渲染一列數(shù)據(jù)(Rendering a List)
先從經(jīng)典需求開始:展示一組條目。
現(xiàn)代前端套路通常是先寫個(gè)返回 JSON 的 API:
// GET /api/items
return [
{ id: 1, name: "Alpha" },
{ id: 2, name: "Beta" },
];前端再去拉取并渲染:
useEffect(() => {
fetch("/api/items")
.then((res) => res.json())
.then(setItems);
}, []);
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>你為“拉、解、存、渲”寫了一堆代碼——把服務(wù)器早就知道的事又重做了一遍。
用 HTMX 的做法:
<div hx-get="/items" hx-trigger="load" hx-target="#list">
Loading...
</div>
<ul id="list"></ul>服務(wù)器直接返回:
<li>Alpha</li>
<li>Beta</li>結(jié)束。沒有本地狀態(tài)、沒有 map、沒有前端邏輯。瀏覽器把片段塞進(jìn)去,繼續(xù)往下走。
分頁(Pagination)
JS 重前端的版本需要本地狀態(tài)、頁碼計(jì)算、請(qǐng)求邏輯:
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`/api/items?page=${page}`)
.then((res) => res.json())
.then(setItems);
}, [page]);你還要處理 loading、點(diǎn)擊事件、依賴項(xiàng)……
HTMX 不關(guān)心這些:
<a href="/items?page=2"
hx-get="/items?page=2"
hx-target="#list"
hx-swap="innerHTML">
Next
</a>
<ul id="list">
<li>Alpha</li>
<li>Beta</li>
</ul>服務(wù)器回一組新的 <li>,HTMX 直接替換內(nèi)容。收工。 沒有本地狀態(tài)、沒有再水合、沒有 effect 依賴。只是渲染下一頁。
表單提交(Form Submission)
做個(gè)簡(jiǎn)單聊天框。
前端式寫法:阻止默認(rèn)、發(fā) JSON、祈禱前后端狀態(tài)對(duì)齊:
const handleSubmit = async (e) => {
e.preventDefault();
await fetch("/api/send", {
method: "POST",
body: JSON.stringify({/* ... */}),
});
// 然后再重新拉消息或手動(dòng)更新狀態(tài)
};你得管理表單狀態(tài)、重置輸入、驅(qū)動(dòng)重渲染。
HTMX 的寫法:
<form hx-post="/send" hx-target="#chat" hx-swap="beforeend">
<input name="message" type="text" />
<button type="submit">Send</button>
</form>
<div id="chat"></div>服務(wù)器收到消息,返回一條氣泡的 HTML。HTMX 把它直接插到列表尾部。 這就是整個(gè)交互。
權(quán)限驅(qū)動(dòng)的 UI(Permission-Based UI)
只想給管理員顯示“Delete”按鈕。
React 里你會(huì)先拉角色,再在客戶端判斷:
{user.role === "admin" ? <button>Delete</button> : null}這意味著在前端復(fù)制一遍后端邏輯,或多傳不該傳的數(shù)據(jù)。
HTMX 讓服務(wù)器自己決定:
<div hx-get="/controls" hx-trigger="load" hx-target="#controls"></div>
<div id="controls"></div>如果是管理員,服務(wù)器返回:
<button hx-delete="/post/123">Delete</button>不是的話就什么也不回。模板里沒有分支,只有權(quán)威事實(shí)。
動(dòng)態(tài)組件(Dynamic Components)
要展示一個(gè) Modal? 前端習(xí)慣:切換狀態(tài)、掛載組件、處理關(guān)閉、動(dòng)畫、Portal……
或者,讓后端直接把整個(gè) Modal 發(fā)過來:
<button hx-get="/user/edit" hx-target="body" hx-swap="beforeend">
Edit User
</button>服務(wù)器響應(yīng):
<div class="modal">
<form hx-post="/user/update">
<input name="name" value="Alice" />
<button type="submit">Save</button>
</form>
</div>沒有“Modal 狀態(tài)”,沒有生命周期鉤子。后端拼好結(jié)構(gòu),前端負(fù)責(zé)展示,你繼續(xù)下一個(gè)需求。
為什么這套路子行得通
HTMX 不會(huì)“魔法般”讓代碼變好,它只是把職責(zé)挪回到那個(gè)本來最懂上下文的層:
- 服務(wù)器已經(jīng)理解 權(quán)限 / 會(huì)話 / 路由 / 查詢參數(shù) / 業(yè)務(wù)規(guī)則 / 邊界情況。
- 讓它說 HTML、返回片段、重新參與 UI,順理成章。
不是所有問題都是“數(shù)據(jù)問題”。 當(dāng)你只是想渲染一個(gè)表單、一個(gè)列表、一個(gè)按鈕,把 JSON 當(dāng)錘子,就會(huì)把一切都敲成釘子。
尾聲
這不是“倒車回老古董”,而是改掉一個(gè)壞習(xí)慣。
我們不再信任服務(wù)器做 UI,于是堆起了客戶端復(fù)雜度的高塔: 為了渲染一個(gè)表單引入客戶端路由;為了禁用一個(gè)按鈕把 props 傳過七層;把“渲染”變成滿地是管道的“水暖工程”。
HTMX 不否定現(xiàn)代開發(fā),它只是否定過度設(shè)計(jì)。 讓服務(wù)器重新掌管 Web。它的能力,比你記憶里的更強(qiáng)。


























