SSR 数据注水是什么?如何避免 XSS 和状态不一致?
简化版
SSR 数据注水是把服务端渲染所需的初始数据序列化到 HTML 中,客户端 hydration 时复用这份数据,避免重复请求和首屏不一致。风险在于序列化不安全会造成 XSS,数据过大拖慢 HTML,数据和 HTML 不匹配会导致 hydration 问题。
详细版
常见形式:
<script id="__STATE__" type="application/json">
{"user":{"name":"Tom"}}
</script>
客户端读取并恢复状态。关键是不要把用户可控内容直接拼到可执行脚本里,要安全转义 <、</script>、Unicode 分隔符等危险字符;敏感字段不要注水到前端;注水数据要和服务端 HTML 使用同一份快照。
完整版教学
一、为什么需要数据注水
SSR 服务端为了生成 HTML 已经取过一次数据。客户端 hydration 后如果再请求一次同样数据,既浪费网络,也可能因为数据变化造成界面不一致。
Server: fetch product -> render HTML
HTML: 商品价格 99
Client: 再 fetch product -> 商品价格 101
Hydration/首屏状态可能抖动
记忆钩子:数据注水就是把服务端用过的那桶水,顺手交给客户端继续用。
二、常见注水方式
<script>
window.__INITIAL_STATE__ = {"product":{"id":1,"price":99}}
</script>
或者使用非执行 JSON 脚本:
<script id="__INITIAL_STATE__" type="application/json">
{"product":{"id":1,"price":99}}
</script>
第二种方式不会直接执行脚本内容,安全性更好一些,但仍要处理 HTML 解析边界。客户端通过 DOM 读取文本再 JSON.parse。
三、XSS 风险来自字符串拼接
如果用户昵称是:
</script><script>alert(1)</script>
直接拼进脚本会提前闭合 script 标签并执行攻击代码。即使你使用 JSON.stringify,也要确保输出嵌入 HTML 时经过适合上下文的转义。
安全序列化至少要把 < 转成 \u003c,避免 </script> 被 HTML 解析器识别:
function safeSerialize(data) {
return JSON.stringify(data).replace(/</g, '\\u003c')
}
真实项目应使用成熟序列化库或框架内置能力,不要手写半吊子转义。
四、不要注水敏感数据
注水数据会进入 HTML,用户能查看源码和 DevTools。服务端为了渲染可能拿到了手机号、权限、内部字段、token,但不代表这些都该发给浏览器。
| 数据 | 是否适合注水 |
|---|---|
| 商品标题和价格 | 适合 |
| 用户显示名 | 视场景 |
| access token | 不适合 |
| 内部成本价 | 不适合 |
| 权限码全集 | 谨慎 |
前端需要什么才传什么,敏感判断必须在服务端完成。不要为了省接口,把整个服务端模型直接序列化到页面。
五、数据大小和性能
注水会增加 HTML 体积。假设首屏 HTML 60KB,注水状态 300KB,首字节后还要下载更多 HTML,解析也更慢。过大的初始状态会抵消 SSR 首屏优势。
合理: 首屏必要数据 20KB
危险: 整个列表、筛选项、用户全量权限 500KB
优化方式包括只注水首屏必要字段、分页加载、压缩传输、把非关键数据延后客户端请求。SSR 不是把所有数据塞进 HTML。
六、保持 HTML 和状态同源快照
服务端渲染 HTML 和注水状态必须来自同一次数据快照。若 HTML 用数据 A,注水用数据 B,客户端恢复后会看到不一致。
render HTML with stateA
serialize stateB
client hydrate with stateB
-> 文本、列表长度、选中状态可能不一致
工程上应把数据获取结果放入统一上下文,渲染和序列化共用同一份对象。不要在渲染后又重新请求一次数据来生成状态。
七、常见误区与追问
- 误区:
JSON.stringify后就绝对安全。 嵌入 HTML/script 上下文仍需处理</script>等解析边界。 - 误区:服务端拿到的数据都可以注水。 注水内容对用户可见,敏感字段必须剔除。
- 误区:注水越多越省请求。 过大 HTML 会拖慢下载和解析,损害首屏。
- 追问:为什么注水能避免重复请求? 客户端 hydration 可复用服务端已取的数据。
- 追问:如何减少 XSS 风险? 使用框架内置安全序列化或成熟库,按 HTML 上下文转义。
- 追问:状态不一致怎么产生? HTML 和注水数据来自不同快照或客户端首次计算不同。
八、加强记忆
SSR 数据注水要记“复用、转义、瘦身、同源”:复用服务端首屏数据,序列化时按 HTML 上下文安全转义,只传首屏必要字段,HTML 和状态来自同一份快照。它能减少重复请求和水合抖动,但一旦处理不好,也会制造 XSS、泄密和性能问题。