服务端预渲染时,必须用html5标准解析器(如parse5、beautifulsoup)将html转为ast再注入状态,禁用字符串拼接或正则替换;json须用json.stringify()序列化并以textcontent写入script标签,优先采用标准格式嵌入dom,确保hydration一致性。

服务端预渲染时,HTML结构里的状态怎么安全注入
不能靠字符串拼接或正则替换往 <div id="app"> 里塞 JSON,那样会破坏 HTML 解析状态,尤其当原始 HTML 含注释、CDATA 或自闭合标签(如 <code><img src="<script>">)时,极易导致属性截断或 XSS。必须用符合 HTML5 标准的解析器,比如 Node.js 的 parse5、Python 的 BeautifulSoup,把 HTML 字符串转成 AST 后再操作节点。
常见错误是直接 htmlStr.replace('', '<script>window.__INITIAL_STATE__ = ...</script>')——这在遇到 或 <!-- comment --> 时就失效。
- 正确做法:用
parse5.parse()解析,遍历 AST 找到节点,在其第一个子节点前插入<script></script>标签 - JSON 数据必须用
JSON.stringify()序列化,并通过textContent写入,避免引号逃逸问题 - 不要把状态写进
<script></script>的innerHTML,否则浏览器可能提前执行脚本
为什么不能把状态挂到 window 上再由客户端 JS 读取
可以挂,但风险集中:如果服务端注入的 window.__INITIAL_STATE__ 被后续脚本覆盖、误删,或客户端 JS 加载失败,整个 hydration 就断了。更稳妥的是把状态嵌进 DOM,比如用 <script type="application/json" id="__NEXT_DATA__"></script> 这类标准约定格式。
这种写法被 Next.js、Remix 等框架采用,原因有三:
- DOM 查询稳定:
document.getElementById('__NEXT_DATA__').textContent不依赖执行时序 - 便于 SSR 和 CSR 共享逻辑:客户端初始化时统一从该节点读取,不耦合 window 属性生命周期
- 利于调试和测试:直接查 DOM 就能看到初始数据,不用打断点看全局变量
模板中动态内容与静态结构如何解耦
关键不是“要不要拆”,而是“在哪一层拆”。服务端模板(如 EJS、Nunjucks)里混写 ... 看似方便,实则让 HTML 结构依赖运行时上下文,难以做静态分析、缓存或预编译优化。
推荐分层策略:
- 结构层(HTML 模板)只保留容器和 slot 占位符,比如
<main data-slot="content"></main> - 数据层(JSON)由服务端按需生成,与模板分离,可独立缓存或 AB 测试
- 组合层(服务端逻辑)用轻量函数把数据映射到 slot,不侵入 HTML 字符串本身
这样改一个文案不用重跑整个模板编译,加一个新字段也不用改 HTML 结构。
容易被忽略的 hydration 边界冲突
服务端吐出的 HTML 和客户端 JS 渲染的 DOM 必须严格一致,否则 React/Vue 会丢弃已有节点、重新 mount,造成事件丢失、滚动位置重置、表单输入清空。最常踩的坑是:
- ID 重复:服务端用了
id="modal-1",客户端又动态生成同名 ID - class 差异:服务端输出
class="btn active",客户端 JS 初始化时漏掉active - data 属性缺失:服务端没写
data-loaded="true",但客户端依赖它判断状态
解决办法不是靠“尽量保持一致”,而是强制约束:所有影响渲染的属性,必须显式声明在服务端数据模型里,客户端只做消费,不二次推导。











