把 Elixir 編譯成 JavaScript,讓同一份程式碼同時撐起伺服端與瀏覽器——這聽起來像是又造了一套要維護的轉譯層,但 Hologram 今年內衝出四個版本,把「幾乎全部」的 Erlang 標準函式庫搬進了瀏覽器,還在 v0.7 那次衝上 Hacker News 首頁 107 分。
Hologram:讓 Elixir 直接跑在瀏覽器
Hologram 想解決的問題不是「Elixir 能不能寫前端」,而是「寫前端的時候,還要不要另外養一整套 JavaScript 工具鏈」。Elixir 生態原本已經有 Phoenix LiveView 把大部分互動邏輯留在伺服器端,但官方部落格自己承認一個縫隙:「LiveView spares Elixir teams most of that - until the app needs real client-side logic, and the JS hooks and npm tooling creep back in at the edges.」需要真正的 client-side 邏輯時,JS hooks 跟 npm 工具還是會從邊緣滲回來。Hologram 的定位就是補上這個縫隙:「Hologram closes that gap. One language, one codebase, no JavaScript stack to maintain.」一種語言、一份程式碼,不用再另外維護一套 JavaScript stack。
這個縫隙不是抽象的工程哲學問題,而是團隊每天都會撞到的分工問題。用 LiveView 寫完後端渲染邏輯之後,一旦畫面需要拖曳排序、即時驗證輸入,或是任何「不等伺服器回應就要立刻反應」的互動,團隊通常得另外寫一段 JavaScript 掛在對應的 DOM 節點上,靠 hook 溝通兩邊的狀態。這段 JavaScript 跟 Elixir 分屬兩套語言、兩套建置流程,審查程式碼的人也得同時看得懂兩邊,才能確認狀態真的同步。Hologram 想拿掉的正是這一段。
一份 Elixir 程式碼,兩個執行環境
撐起這句 slogan 的,是官方列出的一組元件:「a compiler that turns Elixir into JavaScript, a client runtime, a component and template system」。這裡要先說清楚原文實際講了什麼、沒講什麼——編譯器負責把 Elixir 轉成 JavaScript,這是明講的;至於輸出到底是純 JavaScript,還是搭配某種 WASM 的混合形式,原文完全沒有交代,也沒有描述任何中間表示法。這篇不會替官方多加碼,凡是原文沒寫的機制細節,寧可留白或標成推測,也不猜一個聽起來更炫的答案。
能確定的是分工邏輯很乾脆:編譯器只做一件事,把 Elixir 原始碼轉成 JavaScript,轉完就不再過問;剩下的函式呼叫、狀態管理、跟標準函式庫的互動,全部丟給 client runtime 在瀏覽器裡處理。下面這張圖把這條路徑拆成四段,點任一段可以看到它「知道」什麼、「不知道」什麼:
點擊任一個元件看它的責任邊界 · 4 個元件
click a component
Elixir 原始碼 · 共用邊界
官方原文只說「Hologram is a full-stack web framework」與「One language, one codebase, no JavaScript stack to maintain」,沒有描述 build 流程細節(例如編譯器實際跑在哪一台機器)。以下是合理的推測:分界點就落在這份原始碼被送進編譯器之前。
不知道的事:這一層不知道自己最終會被編譯成什麼樣的 JavaScript,也不知道瀏覽器端的 DOM 長怎樣。
Hologram 編譯器 · 責任邊界
原文的元件清單是「a compiler that turns Elixir into JavaScript, a client runtime, a component and template system」——編譯器負責把 Elixir 轉成 JavaScript,但原文並未說明輸出是純 JS 還是搭配 WASM 的混合形式,也沒有描述任何中間表示法,這部分不過度推測。
不知道的事:編譯器不管執行期——它產出 JavaScript 之後,剩下的呼叫全部交給 client runtime。
Client Runtime · 責任邊界
編譯出來的 JavaScript 要能跑,得靠一份移植過的 Erlang 標準函式庫撐著。v0.7 那次搬遷官方寫的是「A community initiative took client-side coverage from a fraction of the standard library to nearly all of it - 150 Erlang functions ported by 49 contributors」——「nearly all」是原文自己的用詞,這篇不會把它升級成「全部」。
不知道的事:runtime 不知道這段程式碼原本是不是也在 server 端跑過一輪,它只認得編譯後的 JavaScript 呼叫。
DOM · 責任邊界
最後一站是瀏覽器畫面本身。原文沒有著墨 DOM diff 或 patch 的演算法細節,只在元件清單裡提到「a component and template system」,渲染機制的具體實作在這份材料裡沒有依據,不寫進本文。
不知道的事:DOM 只反映 client runtime 交給它的結果,不知道背後是不是原本要跑在 server。
這種「先編譯、再靠 runtime 補行為」的設計,在其他把某種語言搬進瀏覽器的專案裡也不是新鮮事——差別通常在 runtime 要補多少東西。如果一個語言的標準函式庫本來就跟 JavaScript 的內建能力很接近,要補的東西就少;Erlang 標準函式庫裡有大量跟並行、訊息傳遞相關的設計,這些概念在瀏覽器單執行緒的 JavaScript 世界裡沒有直接對應,client runtime 得自己想辦法在單執行緒環境裡模擬出「看起來像是有多個輕量級行程在跑」的行為。原文沒有交代這部分實作細節,但這是任何想把 Erlang/Elixir 這類 BEAM 語言搬進瀏覽器都躲不開的問題。
這張圖能回答「誰負責什麼」,但回答不了「這樣切分好不好」。從工程角度看,把編譯器跟執行期切乾淨是常見的健康設計——編譯器的邏輯可以獨立測試、獨立升版,不用擔心動到執行期的行為;執行期也可以在不重新編譯使用者程式碼的前提下修 bug、補功能。Hologram 目前的版本節奏(v0.7 到 v0.11 分屬五個獨立發布)某種程度上印證了這個分工確實撐得住頻繁疊代——如果編譯器跟執行期是黏在一起的大泥球,不太可能在不到一年內連續出五個版本還維持得住。
對正在評估要不要導入的工程主管來說,這裡真正要問的問題是:團隊願不願意把「前端邏輯」這件事,交給一個相對年輕、版本號還在 0 點頭的框架。Hologram v0.7 到 v0.11 的節奏證明它還在快速變動,PCRE 正規表示式、client 端 stacktrace 這些「理所當然」的東西,是今年才逐步補齊,不是從第一天就有的。導入時機的判斷,取決於團隊能不能接受「某個目前還沒補上的能力,可能要等下一個版本才出現」這種風險——這也是為什麼原文特別強調 EEF 的里程碑制資助跟 Curiosum 的長期贊助,而不是只講功能清單:對一個還在快速補齊能力的框架,誰在背後持續投入,跟功能清單本身一樣重要。
把標準函式庫搬進瀏覽器
Client runtime 光靠編譯出來的 JavaScript 呼叫是不夠的——Elixir 程式碼裡到處都是標準函式庫呼叫,這些函式原本是為 BEAM 寫的,要在瀏覽器裡能跑,得先把它們一個一個搬過去。今年最大的一次搬遷發生在 v0.7,官方的說法是:「A community initiative took client-side coverage from a fraction of the standard library to nearly all of it - 150 Erlang functions ported by 49 contributors, most of them making their first contribution to a BEAM project.」涵蓋率從一小部分推到「幾乎全部」,150 個函式,49 位貢獻者,其中多數是第一次替 BEAM 生態的專案送 patch。
「幾乎全部」是原文自己的用詞,不是「全部」——這篇不會把它升級成完整移植。但 49 位貢獻者裡大多數是首次替 BEAM 專案貢獻,這個訊號本身值得注意:代表移植標準函式庫這件事吸引到的不只是核心維護者,還有一批原本不寫 BEAM 生態程式碼的人,願意為了「讓 Elixir 在瀏覽器裡跑」這個目標去讀 Erlang 原始碼、搬邏輯。以下兩個詞後面會用到,先在這裡定義:
BEAMErlang 與 Elixir 共用的虛擬機。標準函式庫裡的每一個函式,原本都假設自己執行在這台虛擬機上,而不是瀏覽器的 JS engine——這也是為什麼「搬」函式庫是一件需要一個一個函式重寫的工程,不是打包編譯就能解決。、 pub/sub fan-out一個發布者的訊息同時發送給多個訂閱者的通訊模式。Phoenix Channels 是 Elixir/Phoenix 生態裡最常見的實作,後面會提到 Hologram Realtime 走的是同一個概念、但重新寫的一套。。
把一個函式從 Erlang 標準函式庫「搬」到瀏覽器,不是找到對應的 JavaScript 函式改個名字就結束。原文用「ported」這個動詞,對應的工程現實通常是:先確認這個函式在 BEAM 上的行為(包含所有邊界輸入、錯誤時丟出的例外型態),再用 JavaScript 重新實作一次同樣的行為,最後驗證兩邊呼叫同一組輸入會得到一模一樣的輸出跟錯誤訊息。150 個函式意味著 150 次這樣的行為比對——這也是為什麼原文特別強調是「幾乎全部」而不是宣稱百分之百,畢竟只要漏比對一種邊界情況,client 端跟 server 端就會在某個角落悄悄行為不一致。
這種「首次貢獻者」佔多數的組成,通常也代表專案本身把貢獻門檻壓得夠低——搬一個標準函式庫函式的工作,理論上不需要理解 Hologram 整個編譯器或 runtime 的架構,只需要理解單一函式在 Erlang 裡的行為,再對照著寫出等價的 JavaScript 版本。這種「可以拆成很多獨立小任務,每個任務不需要通盤理解全貌」的專案結構,本身就比較容易吸引第一次參與的貢獻者——這是從 49 位貢獻者這個數字本身可以合理推論的,但原文並沒有直接說明專案是刻意這樣設計貢獻流程,這部分同樣算是推測。
從 JS interop 到 Regex,四個版本各補一塊
解決了「標準函式庫夠不夠用」之後,接下來四個版本各自把一塊「原本只有 server 端能做的事」搬進瀏覽器。v0.8 開放了 JavaScript interop:「Import npm packages, call Web APIs, use Web Components, and await JavaScript promises as Elixir Tasks」——可以 import npm 套件、呼叫瀏覽器原生的 Web API、使用 Web Components,甚至把 JavaScript 的 promise 包成 Elixir 的 Task 來 await。v0.9 補的是即時通訊層,官方形容為「a full realtime layer - pub/sub fan-out, think Phoenix Channels, but built fresh for Hologram」——不是直接搬 Phoenix Channels 過來用,而是照著同一個 pub/sub fan-out 概念,重新替 Hologram 寫一套。
v0.10 把錯誤處理補齊:「client-side error handling with full try/rescue/catch/after/else in the browser, and comprehension parity」——瀏覽器裡的 try/rescue/catch/after/else 補齊到跟伺服器端一樣完整,list comprehension 的行為也對齊。v0.11 收尾正規表示式:「Elixir's PCRE-based regexes compiled for Hologram's client runtime, with =~ and the rest of the Regex functions, plus client-side error messages and stacktraces」——PCRE-based 的 regex 連同 =~ 運算子都能在 client runtime 裡跑,錯誤訊息跟 stacktrace 也跟伺服器端對得起來。這一版同時是 Erlang Ecosystem Foundation 資助的第四個里程碑收尾。五個版本攤開來看:
| 版本 | 主題 | 這一版做了什麼 |
|---|---|---|
| v0.7 | Elixir 標準函式庫進瀏覽器 | 150 個 Erlang 函式由 49 位貢獻者移植,涵蓋率從一小部分推到「幾乎全部」;該版還衝上 Hacker News 首頁 107 分 |
| v0.8 | JavaScript interop | 可以 import npm 套件、呼叫 Web API、使用 Web Components,並把 JavaScript promise 當 Elixir Task 等待 |
| v0.9 | Hologram Realtime | 新增 pub/sub fan-out 的完整 realtime 層,官方形容為「think Phoenix Channels, but built fresh for Hologram」 |
| v0.10 | 事件與 middleware | 加了 server 端 middleware,client 端補齊 try/rescue/catch/after/else 與 comprehension parity |
| v0.11 | 正規表示式與錯誤訊息 | PCRE-based regex(含 =~ 與其餘 Regex 函式)編譯進 client runtime,外加跟 server 端一致的錯誤訊息與 stacktrace——EEF 第四個里程碑的收尾 |
這四個版本合起來看,補的其實是同一種缺口的四種變體:瀏覽器世界有一整套跟伺服器端不同的原生能力(npm 生態、Web API、事件迴圈、正規表示式引擎),Elixir 原本的寫法沒有直接對應的說法。v0.8 到 v0.11 做的事,基本上都是替這些原生能力找一個道地的 Elixir 說法——JavaScript 的 Promise 對到 Elixir 的 Task,瀏覽器例外對到 rescue/catch,JavaScript 的 regex 語法對到 Elixir 慣用的 Regex 模組跟 =~ 運算子。對已經在寫 Elixir 的工程師來說,這代表學習曲線落在「這個 Web 能力在 Elixir 裡怎麼講」,而不是「要多學一整套 JavaScript 的錯誤處理跟非同步模型」。
「client 端錯誤訊息跟 stacktrace 要跟 server 端一致」這句話,看起來像是細節,但對除錯體驗的影響其實不小。如果同一段 Elixir 程式碼在 server 端丟出的例外訊息長一個樣子,編譯到瀏覽器之後丟出的訊息卻是另一套格式、對應不到原本的行號,工程師除錯時等於要在腦中維護兩套心智模型——一套讀 server log,一套讀瀏覽器 devtools 的 console。v0.11 把這兩邊的訊息格式拉近,某種程度上是在還原「一份程式碼」這個核心主張在除錯這個環節的完整性,而不只是在功能清單上補一項。
跟傳統拆分比一比
把這五個版本合起來看,能明白 Hologram 到底省下了什麼。傳統做法是 Elixir/Phoenix 當後端 API,前端另外一套 TypeScript 或類似框架,兩套語言、兩套型別系統、兩套測試工具鏈,狀態同步邏輯還得手動維護一份在前端、一份在後端。Phoenix LiveView 把大部分場景收斂成一份 Elixir 程式碼,但一旦碰到「必須在瀏覽器裡跑邏輯」的場景,JS hooks 跟 npm 工具就得請回來。下面兩個分頁把兩種做法擺在一起比:
Elixir/Phoenix(含 LiveView)負責大部分互動邏輯,但原文自己點名縫隙所在:「until the app needs real client-side logic, and the JS hooks and npm tooling creep back in at the edges」。一旦踩到這個邊界,團隊得同時維護 Elixir 的型別系統、npm 的套件版本、兩套測試工具鏈,還要手動確保前後端的狀態邏輯不會走鐘。
官方的說法是「One language, one codebase, no JavaScript stack to maintain」。同一份 Elixir 原始碼經過編譯器與 client runtime,直接在瀏覽器裡跑掉原本要靠 JS hooks/npm 補的邏輯——省下的不是「寫得比較快」,而是不用再多養一套獨立的前端工具鏈與心智模型。
這個對比不是說傳統拆分完全沒有道理——當團隊本來就分成後端組跟前端組、各自對自己的技術棧有累積的經驗時,拆分反而是尊重既有分工。Hologram 真正吸引的族群,是那些團隊裡本來就是同一批人在寫 Elixir、卻被迫因為某個互動需求去學一整套前端框架的場景。對這種團隊來說,兩套型別系統帶來的成本不只是多學一種語法,還包括兩邊各自的套件升級節奏、兩邊各自的測試框架、兩邊各自的部署管線——這些成本通常不會出現在功能規格書裡,卻會實實在在地拖慢每一次上線。
從招募的角度看,這個差異也會反映在職缺描述上。傳統拆分的團隊得同時列出 Elixir 跟前端框架兩條技能要求,篩選候選人的池子因此被「兩邊都要會」這件事縮小;如果核心邏輯真的能收斂成一份 Elixir 程式碼,團隊理論上可以只找 Elixir 工程師,把原本要花在「前後端溝通」上的時間,省下來用在其他地方。這個好處能不能兌現,取決於 client-side 邏輯的複雜度有沒有超出 Hologram 目前補齊的能力範圍——如果超出,團隊還是得回頭找懂 JavaScript 的人補洞,跟原文承認的「JS hooks 跟 npm 工具會從邊緣滲回來」其實是同一件事的兩種說法。
資助節奏與採用訊號
Hologram 不是一人專案在自嗨——這篇本身就是一篇募資/近況公告,先把「誰在為它背書」講清楚。Erlang Ecosystem Foundation 的資助形式很具體:「The Erlang Ecosystem Foundation's support came as an agreed program of four development milestones, each one a defined deliverable, not an open-ended arrangement. Three have been delivered and the fourth is finishing now.」四個里程碑、每個都是明確交付項,不是無限期的補助,三個已交付,第四個(也就是上一節提到的 v0.11)正在收尾。日常維運的資金則來自 Curiosum:「Curiosum backs Hologram as Main Sponsor, and that support is what makes full-time work on the framework possible at all.」
里程碑制的資助方式,本身也是一個訊號。相對於「先給一筆錢,之後自由運用」的贊助模式,把資助拆成四個各自有明確交付項的里程碑,意味著 Erlang Ecosystem Foundation 那邊有一份具體的驗收清單,每個階段要看到東西才撥下一筆。這種安排對開源專案的健康度是好事——它逼著開發節奏維持在「看得見進度」的頻率,而不是拿了錢之後躲進去半年沒有動靜。三個已交付、第四個收尾中,對照前面列出的 v0.7 到 v0.11 這五個版本,至少說明資助方跟開發節奏是同步在走的,不是兩條各自為政的時間線。
採用面的訊號也具體。v0.7 那次標準函式庫大搬遷,官方寫的是「The release that concluded it hit the Hacker News front page at 107 points.」上了 Hacker News 首頁,107 分。GitHub 的星數則是「1,400+ GitHub Stars, up 40%+ since November's funding campaign」——去年 11 月那輪募資之後,星數漲了四成以上,來到 1,400 以上。《Programming Elixir》作者 Dave Thomas 的評語是「It really is an amazing piece of work.」——這句話沒有附上更多脈絡,讀者可以把它當成一句業界人士的背書,而不是一份獨立評測。
單一一句業界背書、單一一次上首頁,都不足以單獨構成「這個框架值得信任」的證據,但把三個訊號疊在一起看——技術書籍作者的個人評語、社群討論區的關注度、GitHub star 在募資後的實際成長——至少說明這不是一個只有官方部落格自己在講好話的專案,外部有真實的人在關注、在測試、在給意見。這跟「這個框架技術上到底做得好不好」是兩件事:後者需要工程師自己去讀程式碼、跑基準測試才能判斷,前者只能說明「值得花時間去讀程式碼」這個判斷本身是合理的。
不過這些訊號都需要一點提醒:這是一篇募資/近況部落格,不是技術規格文件。原文提到「the first applications running in production」,但沒有交代規模,沒有交代是哪些應用,也沒有效能數字——「production」在這裡是自報的狀態,不是第三方驗證過的案例研究。標準函式庫的移植也只到「幾乎全部」,邊界情況、效能開銷這些工程師真正想知道的細節,這篇募資文完全沒有觸及。
要把這個路線圖從「positioned to become」變成現實,合理的推測是還有兩塊硬骨頭要啃:一塊是離線期間的衝突解決——使用者在沒有連線時修改的資料,重新上線後要怎麼跟伺服器端的版本合併,這向來是 local-first 系統裡最難的一段,原文完全沒有著墨用的是哪一種演算法或協定;另一塊是三個平台共用同一份 Elixir 程式碼時,平台各自的原生能力(推播通知、檔案系統存取、桌面視窗管理)要怎麼在同一套抽象裡曝露出來又不互相拖累。這兩塊原文都沒有給答案,標成推測是因為目前沒有材料可以再往下講。
這也是為什麼把「positioned to become」這幾個字留在引號裡讀,比直接省略掉更重要。路線圖式的語言在募資文裡很常見,原因很直接——這類文章的讀者除了工程師,還包括潛在的贊助方跟合作夥伴,把方向講清楚本來就是這類文章的功能之一。工程師讀這類文章真正該做的事,不是把路線圖當成產品規格書來讀,而是把它當成「這個專案接下來打算往哪個方向投入資源」的訊號——如果 offline-by-default 真的在下一輪出現,那才是真正該去讀技術細節、驗證邊界情況的時間點。
What this enables:官方把接下來的方向講得很直白——「Hologram is positioned to become the first full-stack framework in any ecosystem with local-first built in. Applications that are offline by default: they keep working without a connection, hold the data you choose on the client, and sync automatically when the connection returns - backed by a declarative auto-syncing data layer instead of hand-rolled plumbing.」加上多平台的野心:「One Elixir codebase, running on the web, on mobile and the desktop, offline by default and syncing across all of them.」這兩句都還是「positioned to become」的路線圖語氣,不是已經出貨的功能——但如果 v0.7 到 v0.11 這個節奏(不到一年五個版本)繼續下去,offline-by-default 加上一份 Elixir 程式碼跑三個平台,會是下一個該盯的版本。