EJS、Pug、Handlebars 默认不支持浏览器直接运行,因其依赖Node.js环境的fs、require、res.render等服务端能力;浏览器无文件系统和模块加载机制,故需引入浏览器版SDK、内联模板并手动编译渲染。

为什么不能直接在浏览器里用 EJS / Pug / Handlebars 渲染 HTML?
你写完 EJS 或 Pug 模板,直接丢进 index.html 里用浏览器打开——页面是空白的,或者报错 ReferenceError: require is not defined。这不是模板写错了,而是它们默认不支持纯前端运行。这些引擎设计初衷是服务端渲染(Node.js 环境),依赖 fs 读文件、require 加载模块、res.render() 响应请求。浏览器没有这些能力,也没法执行 这类服务端逻辑。
想在浏览器里跑,必须:
- 手动引入对应引擎的「浏览器版」构建(如
handlebars.min.js,不是 npm 包本身) - 把模板写成
<script type="text/x-handlebars-template"></script>标签内联,或用fetch()加载字符串 - 显式调用
Handlebars.compile()+template(data)手动渲染,再innerHTML插入 - 放弃
include、extends等需要文件系统的能力(Pug 尤其难搞)
EJS 和 Handlebars 在前端使用时的关键差异
EJS 语法接近 JS,可以直接写 { %>,但这也意味着它容易把业务逻辑塞进模板,调试时 JS 错误和模板错误混在一起;Handlebars 是「逻辑无关」的,强制你提前把 users 处理成扁平数组,模板里只允许 {{#each users}} 这种声明式写法,出错更干净,但准备数据的成本更高。
常见坑点:
立即学习“前端免费学习笔记(深入)”;
-
EJS的默认不转义,容易 XSS;必须手动写 <code> 或加过滤器,而前端没后端那套自动上下文转义 -
Handlebars的{{foo}}默认转义,要输出 HTML 得写{{{foo}}},多一层花括号容易漏,且不校验内容是否真为安全 HTML - 两者都不支持
import/require,无法复用 JS 工具函数,得提前挂到window或传进data
Pug 在浏览器中几乎不可行的原因
Pug 依赖缩进和换行生成结构,它的编译器(pug.compile())在浏览器里能跑,但问题不在编译,而在加载:你没法用 require('xxx.pug'),也没法像 Node 那样 fs.readFileSync 同步读取本地 .pug 文件。即使把 Pug 字符串硬编码进 JS,一旦模板含 include 或 extends,就彻底卡死——浏览器没文件系统,也没服务端的模板解析上下文。
真实限制:
- 所有
include、extend、block必须提前在构建时展开(比如用pug-cli --watch预编译成 HTML 或 JS 函数) - 无法动态加载子模板,每次改一个
header.pug就得重新打包整个前端资源 - 错误提示极不友好:
Unexpected token ILLEGAL往往只是缩进多了一个空格,但在浏览器控制台看不出哪行
真正适合前端直用的其实是 tagged template literals
如果你只是想摆脱字符串拼接、又不想引入重型模板引擎,lit-html 或原生 String.raw + 自定义 tag 函数才是轻量解法。它们不解析模板字符串,而是靠 JS 引擎原生支持,性能高、无运行时依赖、类型提示友好。
例如:
const render = (data) => html`<h1>${data.title}</h1><p>${data.content}</p>`
对比下来:
-
lit-html支持响应式更新、自动转义、与 Web Components 深度集成,但需额外引入包 - 纯
String.raw+ 正则替换最简,但无转义、无嵌套、无条件逻辑,仅适合静态片段 -
EJS/Handlebars/Pug的浏览器用法本质是「降级使用」,牺牲了它们最核心的能力(文件组织、服务端预处理、流式渲染),只换来一点语法糖
真正卡住多数人的,从来不是“哪个模板语法更顺手”,而是“怎么让 include 路径在 dev 和 prod 下都对得上”——这个问题在浏览器里没有优雅解,只有绕开。



















