模板引擎选型需匹配渲染时机:ssr用nunjucks/handlebars,csr用框架原生方案;data-*仅作初始化钩子,状态控制用class/hidden;web components是唯一可控的data-变更响应路径。

模板引擎选型必须匹配渲染时机
服务端渲染(SSR)和客户端渲染(CSR)对模板引擎的要求完全不同。Handlebars 和 Nunjucks 适合 SSR,因为它们支持同步编译、可预编译、能直接嵌入 Node.js 环境;而 mustache 无逻辑、纯字符串替换,虽安全但无法处理条件分支,硬塞进 CSR 场景会逼你用 JS 拼接 HTML,极易 XSS;Pug 缩进语法在团队协作中容易因空格错位导致编译失败,且不支持原生浏览器运行——它必须经构建工具转成 HTML 才能部署。
常见错误现象:
- 在 Vue 项目里用
handlebars.compile()渲染模板,结果{{ message }}被 Vue 的响应式系统忽略,变成静态文本 - 把
nunjucks.renderString()放在 React 组件的useEffect里调用,却忘了它返回的是字符串,不是 React 元素,直接赋给innerHTML会丢失事件绑定 - 用
mustache.render()处理含{{#if}}的模板,但传入数据里没定义if字段,模板直接报错退出,页面白屏
实操建议:
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
- CSR 场景优先用框架原生方案:Vue 用
<template></template>+v-if,React 用 JSX,别额外引入模板引擎 - SSR 场景若用 Express,选
nunjucks并配置app.set('view engine', 'njk'),避免手写res.send(nunjucks.render(...))导致路径拼错 - 纯静态生成(如 Hugo、Jekyll)用
liquid或markdown+ front matter,别强套 JS 模板引擎,构建时解析更稳
data-* 属性不能当状态源,但能当初始化钩子
data- 属性只该用于传递不可变的初始参数,比如 data-theme="dark"、data-api-endpoint="/v1/users"。它不是响应式通道,浏览器不会监听它的变更,JS 修改后也不会触发重绘。把它当状态用,等于埋了个类型+时序双重雷。
常见错误现象:
-
el.setAttribute('data-count', 42)后,组件里用this.dataset.count读出来还是"42"字符串,参与++运算变成"421" - SSR 渲染出
<div data-filters='{"sort":"date"}'>,客户端 JS 直接 <code>JSON.parse(el.dataset.filters),没加try/catch,遇到单引号或转义失败就崩溃 - 自定义元素构造函数里访问
this.dataset.id,但此时 DOM 尚未挂载,值为undefined,后续逻辑全断 - 组件 mount 后立刻执行
const theme = el.dataset.theme === 'dark' ? true : false,存进内部 state,之后只读 state,不碰 dataset - 结构化数据一律用
el.getAttribute('data-config')+try { JSON.parse(...) } catch (e) { {} } - 禁止在 SSR 模板里动态插值
data-值,例如data-timestamp="{{ now() }}"—— 服务端和客户端时间不同步,diff 会误删/重复节点 - 显隐控制统一用
el.hidden = true/false,别用el.setAttribute('hidden', '')—— property 和 attribute 不同步会导致状态错乱 - 多态 UI(loading/success/error)用
el.classList.toggle('state-loading'),配合 CSS 的.state-loading .spinner规则,比切换style.display更利于维护 - 避免用
title控制 tooltip 显示逻辑——它只被屏幕阅读器读取,鼠标悬停靠 CSS 的:hover或 JS 的mouseenter,两者不该混用 - 声明
observedAttributes列表,只监听真正需要响应的data-属性,避免过度响应 - 在
attributeChangedCallback里做类型转换和副作用,例如if (name === 'data-count') this._count = Number(newValue) || 0 - 不要在回调里直接操作 DOM 渲染,先更新内部 state,再由
render()统一输出,保持响应式一致性
实操建议:
class 和 hidden 是唯一可靠的状态输出通道
class 和 hidden 是少数几个属性变更后,浏览器会立即触发样式重绘或 DOM 隐藏/显示的全局属性。它们不存状态,但能稳定驱动视图变化。其他如 title、lang、tabindex 只影响辅助技术或焦点流,不能替代 UI 状态控制。
实操建议:
Web Components 是唯一可控的 data- 变更响应路径
如果非要用 data- 属性做参数并响应变更,唯一合规路径是走 customElements.define() + attributeChangedCallback。原生 div 或 span 加了 data-id,没有任何机制通知 JS “它变了”。
实操建议:
复杂点在于:每个自定义元素都要手动管理 attribute → state → render 的链路,没法像 Vue 的 v-model 或 React 的 useState 那样自动同步。容易被忽略的是,attributeChangedCallback 不会在初始渲染时触发——首次值来自 HTML,得在 connectedCallback 里补读一次 this.dataset。










