我的第一個 npm 套件:2011 年的 Node 模板引擎 jst

· 6 min read

翻 npm 套件清單時,發現自己最早的發布記錄停在 2011 年。那是我寫的第一個 npm 套件:jst(Node JavaScript Template)——一個號稱 pretty high performance 的模板引擎。

這篇不是要推銷一個 15 年沒人維護的套件。它是數位考古,也是後面兩個仍在維護的路由庫 —— alpinejs-routersvelte-router —— 的前情提要:同一個作者,同一種執念,中間隔了十多年。

In English: This is the only Chinese-language post in a four-part series about my open-source packages. jst was my first npm publish — a Node template engine from April 2011, back when Node was v0.4 and npm was 0.3.x. It stopped at 0.0.13 and I am no longer its maintainer, but the design instincts it taught me (compile once and cache, keep the API surface small, qualify every performance claim) are the same ones behind the routers I still ship. If you came here from the Alpine, Svelte or simpleconf posts, those are all in English.

2011 年的 Node 世界

那時 Node.js 還在 v0.4npm0.3.x,registry 上的套件屈指可數。幾乎每個 Web 框架都需要模板引擎 —— EJS、Jade(現在的 Pug)都是那個時代的產物。我寫 jst 的目標很單純:輕量、夠快、API 好記

發布時間線很短、很密:

版本 日期
0.0.1 2011-04-28
兩週內連續迭代
0.0.13 2011-05-08(至今仍是 latest)

那段時間我是真的在用它:Express 註冊 .html、layout、i18n、filters,一條龍。

jst 長什麼樣

語法接近「HTML + 一點 JS」,有兩種插值:

  • {{ expression }} — 輸出變數(可接 filter)
  • {% code %} — 嵌入任意 JS 控制流
{% if (user) { %}
  <h2>{{ user.name }}</h2>
{% } %}
var jst = require('jst')

// 字串
jst.render('Hello {{ name }}', { name: 'jst' })

// 編譯成函式後重複使用
var fn = jst.compile('Hello {{ name }}')
fn({ name: 'jst' })

// 用 it. 前綴可避開 with,官方 README 聲稱可快一個數量級以上
jst.render('Hello {{ it.name }}', { name: 'jst' })

// filter 鏈
jst.render('Hello {{ it.name|e }}', { name: '<strong>jst</strong>' })

Express 時代的接法(節錄自當年 example):

var express = require('express')
var jst = require('jst')
var app = express.createServer()

app.register('.html', jst)
app.get('/', function (req, res) {
  res.render('index.html', { title: 'Express' })
})

layout 也是模板自己拼:

<!DOCTYPE html>
<html>
  <head><title>{{ title }}</title></head>
  <body>
    {{ body }}
    {{ partial('footer') }}
  </body>
</html>

現在 npm 上還看得到什麼

npm view jst
  • 描述:Node JavaScript Template, A pretty high performance template engine
  • 最新版本:0.0.13(2011-05-08)
  • 原始作者欄:Shaun Li <[email protected]>
  • 目前 maintainer 已轉給他人(nick_d2)—— 套件還在 registry,但我已不是維護者

「pretty high performance」是 2011 年 README 的自我標榜,對照的是當年 jsPerf 上各家模板引擎的短模板比拚;不是今天的正式 benchmark 聲明。有趣的是核心思路並不土:中間碼快取、可選 it. 前綴避開 with、filter 管線 —— 和後來寫 router 時對「熱路徑要短、API 要穩」的要求是同一條線。

三件事,到現在還受用

1. 開源是長跑,不是一輪 PR

從 2011 的 jst,到 2022 起的 @shaun/alpinejs-router@shaun/svelte-router,中間隔了十多年。中間有空白、有換棧、有不再維護的實驗。持續有東西掛在自己的 namespace 下,本身就是一種可驗證的履歷 —— 比「我當年寫過」有用得多。

2. 命名就是 SEO,也是命運

jst 太普通了。搜尋引擎裡它是歧義詞(JavaScript Template、各種組織縮寫),等於把發現成本讓給別人。對比之下:

名字 搜尋意圖
jst 幾乎無法精準命中
@shaun/alpinejs-router 用途 + 生態直接寫進名字
@shaun/svelte-router 同上,scoped 還避免搶全局名

scoped package(@shaun/...)不只是組織方式,也是可擁有的搜尋與信任邊界。若 2011 年有 npm scope,我大概會叫它 @shaun/jst 或直接把「template」寫進名字。

3. 技術會過時,判斷力會留下

模板引擎後來被框架內建、JSX、單檔元件吃掉大半生存空間。jst 停在 0.0.13 是合理結局。但當年練過的東西沒丟:

  • 字串編譯與快取(router 的 match 表也是「編譯一次、熱路徑查表」)
  • API 表面要小(render / compile / renderFile 三個入口 vs 現在的 createRouter + Link + View
  • 效能宣稱要附條件(jst 的 jsPerf 連結 vs 現在 router / simpleconf 文裡的 microbenchmark disclaimer)

過時的是語法糖;留下的是「什麼該進核心、什麼不該」的直覺。

一條沒斷的線:編譯一次,熱路徑查表

jst 最核心的一招,是把模板字串編譯成函式後快取:解析只做一次,之後每次 render 都只是呼叫一個已經存在的函式。這在 2011 年是模板引擎的常識,但它變成了我後來所有東西的預設姿勢。

十四年後寫路由器,形狀完全一樣,只是換了名詞:

jst(2011) alpinejs-router / svelte-router(2022–)
編譯一次的東西 模板字串 → JS 函式 路由字串 → RegExp(named groups)
快取在哪 模板 → 函式的表 路由 → pattern 的表;靜態路由連編譯都省了
熱路徑做什麼 呼叫已編譯函式 Set.has() / Map.get(),必要時才跑 regex
為什麼要避開的東西 with 語句(所以有 it. 前綴) 對整張路由表做線性掃描(所以有兩層索引)

差別在於,2011 年的我只知道「快取編譯結果會比較快」;現在會多問一句沒命中的時候要多花多少。兩個路由器的 benchmark 裡都特地列了 miss 的數字,就是這個習慣的產物 —— 一個只在 happy path 快的東西,不算快。

FAQ

jst 現在還能用嗎?
理論上還能安裝(npm install jst),但不建議。它停在 2011 年的 0.0.13,寫的是 Node v0.4 時代的 API,例子裡的 express.createServer() 早就被移除了。這篇是紀錄,不是推薦。

你還是 jst 的維護者嗎?
不是。npm 上的原始作者欄仍是我,但 maintainer 已經轉給 nick_d2。套件本身還留在 registry。

為什麼不把它重寫成現代版?
因為這個問題已經被解決了。模板引擎的生存空間被框架內建的模板、JSX 和單檔元件吃掉了,再寫一個沒有意義。我把同樣的設計偏好放進了還有人需要的東西 —— 客戶端路由和設定服務。

「pretty high performance」到底有多快?
以 2011 年的標準、在當年 jsPerf 的短模板比拚裡不算慢。這是當時 README 的自我標榜,沒有可重現的 benchmark,也不該被當成今天的效能宣稱來讀。

從模板引擎到路由器的十四年

若對我現在仍在維護的套件有興趣:

從「把模板塞進 HTML」到「在瀏覽器裡管理歷史與路由」,再到「一個 binary 的設定服務」,核心其實沒變:

設計一個小而精、好用的 API,讓別人願意用它 —— 也讓幾年後的自己還看得懂。

jst 完成了它的歷史任務。名字起得普通、生命週期很短,但作者欄還在 npm 上。對我來說,那已經夠當一塊里程碑。