技术解读
1 KB 的模板引擎还能预编译:拆解 template.js 的设计取舍
项目背景与痛点
模板引擎的黄金年代似乎已经过去,但“把数据填进一段字符串”的需求从未消失:静态站点生成、Node 服务端渲染 HTML 片段、邮件模板、无框架的 Web Component、SDK 里内嵌的少量视图——这些场景既不需要 Vue 的响应式,也不需要 React 的调度,只需要一个可靠的字符串渲染器。
现成的方案各有代价。Handlebars 光运行时就有几十 KB;art-template 功能齐全,体积同样不轻;EJS 语法友好,但工程化链路偏向服务端;doT 极小,却几乎停止了迭代。更普遍的痛点是模板引擎与构建链路割裂:预编译通常要额外的 CLI 或手写脚本,与现代 bundler 的集成并不顺手。
yanhaijing/template.js 的策略很清晰:把引擎压到几乎可以忽略的体积,同时把预编译能力做成构建工具的一等公民。README 里那一长串 webpack / rspack / vite / rollup / esbuild / parcel / browserify / fis / gulp,从最新到几乎绝迹的工具一家不落——这说明作者真正瞄准的用户是维护存量工程的人,而不是只写新项目的开发者。
核心技术揭秘
template.js 在结构上分为“编译器”和“运行时”两半,这种分离是它体积小的根本原因。
编译器做的是典型的 scan → codegen 两段式工作:用正则扫描模板中的 <%= %>(转义插值)、<%- %>(原样插值)、<% %>(逻辑语句)这几类标记,把模板切成静态文本块与代码块组成的 chunk 序列。随后拼接函数体源码:静态文本直接变成字符串字面量输出,代码块原样嵌入函数逻辑。最终产物是一个可执行函数的源码字符串,交由 new Function 构造出渲染函数。
这里的核心取舍在数据作用域。模板里可以直接写 title 而不用写 data.title,靠的是让数据对象成为函数执行上下文的一部分。这种实现让模板语法极其自然,代价是它触碰了严格模式下禁用 with 这一限制,V8 也很难对这类动态作用域做优化。若换成参数解构方案,语义会更干净,但模板写起来会更啰嗦——template.js 选择了偏重易用性的一侧。
运行时则尽量精简:核心是 escape 实现和少量工具函数,<% 逻辑由编译产物自己承担。模板字符串到编译函数的映射会被缓存,因此热路径上重复 render 同一模板时不会重复编译。
预编译是它最有价值的工程能力:把编译器产出的函数源码交给对应 bundler 的 loader/plugin,在构建期写进 bundle,运行时只做“还原并执行”,跳过扫描与 codegen。
功能亮点与差异
体积是第一杀手锏:压缩后的运行时在 1 KB 量级,这意味着你可以把它塞进任何一个库而无须解释“为什么多了 30 KB 依赖”。
构建工具覆盖度是第二亮点,且罕见。多数同类项目只维护 webpack 与 vite 两个插件,template.js 连 fis、gulp、browserify 这类处于后期的工具都没放弃,对老项目的迁移非常友好。
第三是语法亲和力。它与 EJS 高度相似,<%= %> 的直觉几乎不需要学习成本,存量 EJS 模板改造成本接近于零。
差异化的地方也很明确:相比 art-template,它没有过滤器管道、模板继承、子模板 include 的完整体系;相比 EJS,它的预编译链路更顺、体积更小。它有意只做一个“最小可用集”。
应用场景与落地建议
适合:静态站点/SSG 的片段渲染、Node 侧轻量 HTML 拼装、邮件模板、无框架页面与 Web Component、需要在 SDK 内嵌极小视图库的场景。
落地时需要注意以下几点:
- CSP 是最大隐患。
new Function依赖unsafe-eval,在浏览器扩展、金融后台、启用严格 CSP 的页面中会直接失效。这类环境必须走 loader 预编译路径,并确认产物是可直接执行的模块,而不是运行时仍需 eval 的字符串——这是最容易出错的地方。 - XSS 边界要划清。默认用
<%= %>,只有确认内容本身就是可信 HTML 时才使用<%- %>。 - 不要在模板里写业务逻辑。长段 JS 塞进
<% %>会让模板退化成难以维护的“字符串里的程序”。 - 确保命中缓存。在热路径上提前编译好函数并复用,不要每次 render 都传模板字符串。
综合评价
优点很突出:把“极小体积”和“能接入现代/存量构建链”两件事同时做到了,API 克制、零依赖、学习成本几乎为零,是性价比极高的选择。
短板同样清晰:单维护者项目,功能面窄,缺少布局继承与过滤器这类体系化能力;错误信息与源码映射不会像成熟框架那样精细;严格 CSP 场景需要额外工程处理;with 的历史包袱也限制了它在某些模块环境下的直接使用。
一句话总结:如果你要的是“能预编译、能进 bundle、体积可以忽略”的模板能力,它几乎是最优解;如果你要的是一整套模板体系(布局、过滤器、国际化插桩),请直接去看 art-template 或 EJS。
评论