為何 htmx 沒有建置步驟

Alexander Petros

一些 htmx 貢獻者經常問一個問題,就是為什麼 htmx 不是用 TypeScript 編寫,或者說,為什麼 htmx 完全沒有任何建置步驟。完整的 htmx 原始碼是一個單一的 3,500 行 JavaScript 檔案;如果你想為 htmx 貢獻程式碼,你只需修改 htmx.js 檔案,這個檔案與發佈到生產環境瀏覽器的檔案相同,只做過最小化和壓縮處理。

我不能代表 htmx 專案發言,但我對它做了一些重要的貢獻,並且每次出現這個問題時,我都一直是保留這種無建置設置的積極倡導者。從我的角度來看,以下是 htmx 沒有建置步驟的原因。

#一次編寫,永遠執行

用純 JavaScript 編寫程式庫的最佳理由是它可以永遠存在。這可以說是 JavaScript 最被低估的特性。雖然我確信有一些極端情況,但 1999 年在 Netscape Navigator 中運行的 JavaScript 程式碼,仍然可以在昨天下載的 Google Chrome 中與現代程式碼一起運行。對於很少的程式設計環境來說是這樣。對於 Python、Java 或 C 來說肯定不是這樣,它們都有版本機制,選擇新的語言特性會迫使你放棄已棄用的 API。

當然,大多數人對 JavaScript 的經驗是它像牛奶一樣容易變質。在 3 個月後重新開啟一個 node 儲存庫,你會發現你的專案陷入了安全警告、不相容的程式庫「升級」和一個前端框架的混亂之中,這個框架的文化巔峰時刻正是你開始這個專案的時候,現在卻被廣泛認為是技術債。這種情況該歸咎於誰,這應該由其他人來決定,但無論如何,你可以透過不依賴 JavaScript 執行環境以外的任何依賴關係來消除整個問題類別。

現在流行的 JavaScript 編寫方式是從 TypeScript 編譯(我會經常使用它作為範例,因為 TypeScript 可能是使用建置系統的最佳理由)。TypeScript 無法在網頁瀏覽器中原生執行,因此 TypeScript 程式碼不受 ECMA 對向後相容性的狂熱奉獻所保護。像任何依賴關係一樣,新的主要 TypeScript 版本不能保證與之前的版本向後相容。它們可能相容!但如果它們不相容,那麼如果你想使用現代開發工具鏈,就需要進行維護。

維護是用勞動力支付的成本,而開放原始碼程式碼庫是最難以負擔這種成本的專案。選擇不使用建置步驟可以大幅減少讓 htmx 保持最新所需的勞動力。這個經驗已由 intercooler.js 證實,它是 htmx 的前身,它在(據我所知)非常少量的努力下無限期地維護著。當 htmx 1.0 發佈時,TypeScript 的版本是 4.1;當 intercooler.js 發佈時,TypeScript 的版本是 1.0 之前。用這些 TypeScript 版本編寫的程式碼是否能在今天的 TypeScript 編譯器(在撰寫本文時的版本為 5.1)中未修改地編譯?也許可以,也許不行。

但是 htmx 是用 JavaScript 編寫的,沒有任何依賴關係,因此只要網頁瀏覽器仍然相關,它就可以在未修改的情況下執行。讓瀏覽器供應商為你做繁重的工作。

#開發人員體驗

確實,在許多方面,TypeScript 的開發人員體驗 (DX) 比 JavaScript 的開發人員體驗更好。但 TypeScript 的 DX 並非在所有方面都更好,而且軟體工程師傾向於將進步視為能力的目標論,而不是帶有權衡的選擇,這有時會讓他們對他們喜歡的 DX 方面所付出的代價視而不見。例如,使用 TypeScript 的一個小權衡是,編譯它需要時間,而且你必須等待它重新編譯才能測試變更。通常這種成本可以忽略不計,而且很值得付出,但它仍然是一種成本。

使用 TypeScript 的一個更重要的成本是,在瀏覽器中執行的程式碼不是你編寫的程式碼,這使得瀏覽器的開發人員工具更難使用。當你的 TypeScript 程式碼拋出例外時,你必須弄清楚堆疊追蹤(帶有其 JavaScript 行號、JavaScript 函數簽名等)如何對應到你編寫的 TypeScript 程式碼;當你的 JavaScript 程式碼拋出例外時,你可以直接點擊到原始碼,閱讀你編寫的東西,並在除錯工具中設定斷點。這是非常棒的 DX。對於許多從未這樣工作過的年輕網頁開發人員來說,這可能是一次啟示性的體驗。

建置步驟的擁護者指出,TypeScript 可以產生來源對應,它會告訴你的瀏覽器哪個 TypeScript 對應到哪個 JavaScript,這是真的!但現在你有了另一個需要追蹤的東西 — 你編寫的 TypeScript、它產生的 JavaScript 以及連接這兩者的來源對應。你現在依賴的熱重新載入開發伺服器會在 localhost 上為你保持這些更新 — 但在你的預備伺服器上呢?在生產環境中呢?在這些環境中出現的錯誤會更難追蹤,因為你已經遺失了許多關於它們來源的資訊。這些都是可以解決的問題,但它們是你創造的問題;它們是一種成本。

htmx DX 非常簡單 — 你的瀏覽器載入一個單一檔案,在每個環境中都是你編寫的完全相同的檔案。維持這種體驗所需的權衡是真實的,但它們對於這個專案來說是有意義的權衡。

#強制清晰度

模組化是軟體的絕佳想法之一。模組使我們可以將程式碼分解為解決較小問題的良好封閉子結構,從而解決極其複雜的問題。模組非常有用。

但是,有時你只想解決簡單的問題,或者至少是相對簡單的問題。在這些情況下,使用更複雜軟體的建構區塊可能會有所幫助,以免你模仿它們的複雜性而沒有產生相應的價值。htmx 的核心是解決一個相對簡單的問題:它在 HTML 中加入少量的屬性,使其更容易使用超文字的宣告性來取代 DOM 元素。要求 htmx 保持在單一檔案中(再次強調,大約 3,500 行程式碼)會在程式庫上強制執行一定程度的意圖;在處理 htmx 原始碼時,會存在一種真實的壓力來證明加入新程式碼是合理的,這種壓力維持著相對簡單的平衡。

雖然 DX 成本很明顯,但也有令人驚訝的 DX 好處。如果你在原始碼檔案中搜尋函數名稱,你會立即找到該函數的每個調用(這也減少了對更進階程式碼自省的需求)。缺少讓功能隱藏的地方讓處理 htmx 變得更容易上手。更複雜的專案也使用了這種方法的一些方面:SQLite3 從一個單一檔案來源合併編譯(雖然它們為開發使用單獨的檔案,但它們並非瘋狂),這使得對它的 hack 變得更容易。你永遠無法以這種方式建置 Linux 核心 — 但 htmx 不是 Linux 核心。

#成本

像任何技術決策一樣,選擇放棄建置步驟有利有弊。重要的是要承認這些權衡,以便你可以做出明智的決定,並在某些好處或成本不再適用時重新審視該決定。考慮到編寫純 JavaScript 的優點,讓我們考慮一下它引入的一些痛點。

#沒有靜態型別

TypeScript 是 JavaScript 的嚴格超集,它新增的一些功能非常有用。TypeScript 有…型別,這使得你的 IDE 更擅長建議程式碼,並指出你可能錯誤使用方法的地方。自動重新命名和重構程式碼的工具對於 TypeScript 來說比對於 JavaScript 更可靠。但是,htmx 程式碼必須用 JavaScript 編寫,因為瀏覽器執行 JavaScript。而且,只要 JavaScript 是動態型別,在 htmx 原始碼中取得真正靜態型別所需的權衡就不值得(htmx 使用者仍然可以利用使用 .d.ts 檔案宣告的型別化 API)。

未來版本的 htmx 可能會使用 JSDoc 來獲得一些相同的保證,而無需建置步驟。其他程式庫,例如 Svelte,也一直朝這個方向發展,部分原因是 TypeScript 檔案引入的除錯摩擦

#沒有 ES6

因為 htmx 維持對 Internet Explorer 11 的支援,而且因為它沒有建置步驟,所以 htmx 的每一行都必須用與 IE11 相容的 JavaScript 編寫,這表示沒有 ES6。當像我這樣的人說現在 JavaScript 很好時,他們通常指的是 ES6 引入的語言功能,例如 async/await、匿名函數和函數式陣列方法(即 .map.forEach) — 這些都不能在 htmx 原始碼中使用。

雖然這非常煩人,但實際上它並不是一個巨大的障礙。缺少一些不錯的語言功能並不會阻止你編寫具有函數式範式的程式碼。不想編寫自訂的 forEach 方法會不會很好?當然。但在 htmx 定位的所有瀏覽器都支援 ES6 之前,用一些 helper 函數補充 ES5 並不難。如果你習慣使用 ES6,你會自動編寫更好的 ES5。

IE11 支援將在 htmx 2.0 中被捨棄,屆時原始碼中將允許使用 ES6。

#核心中沒有模組

這一點很明顯,但值得重申:如果 htmx 原始碼可以分割成模組,它會整潔得多。影響程式碼品質的因素除了整潔之外還有其他因素,但在 htmx 原始碼是高品質的範圍內,它並不是因為它整潔。

這使得使用 htmx 做某些事情非常困難。idiomorph 演算法可能會包含在 htmx 2.0 核心中,但它也作為一個單獨的套件維護,以便人們可以使用 DOM 形變演算法而無需使用 htmx。如果核心可以包含多個檔案,可以使用任何數量的鏡像方案(例如 git 子模組)輕鬆完成此操作。但核心是一個單一檔案,因此 idiomorph 程式碼也必須存在於其中。

#最終想法

這篇文章或許更適合的標題是「為什麼 htmx 目前 沒有建置步驟」。如同先前所述,情況會改變,這些權衡隨時都可以重新審視!我們目前正在探索的一個問題與發佈版本有關。當 htmx 發佈版本時,它會使用一些不同的 shell 命令來用 htmx.js 的精簡和壓縮版本填充 dist 目錄(學究們可以指出,從某種意義上來說,這顯然是一個建置步驟)。未來,我們可能會擴展該腳本以自動生成通用模塊定義。或者我們可能會出現新的發佈需求,需要更複雜的設定。誰知道呢!

htmx 的核心價值之一是,在過去十年來一直被日益複雜的 JavaScript 堆疊所主導的網頁開發生態系統中,它為您提供選擇。一旦您不再擁有龐大的前端 JavaScript 程式碼庫,在後端採用 JavaScript 的壓力就會小得多。您可以使用 Python、Go,甚至是 NodeJS 編寫後端,這對 htmx 來說都沒關係——每一種主流語言都有成熟的解決方案來格式化 HTML。這就是隨心所欲使用超媒體 (Hypermedia On Whatever you’d Like, HOWL) 的原則。

一旦您不再需要 NextJS 或 SvelteKit 來管理 SPA 框架不斷上升的複雜性,那麼在沒有建置流程的情況下編寫 JavaScript 就是您可以選擇的選項之一。這種選擇對今天的 htmx 開發來說是有意義的,但對於您的應用程式來說,可能不一定有意義。

</>