玩转SSR之数据脱水

170 阅读4分钟

当你使用 React 的 SSR 时,你可能会遇到一个问题,就是你在服务端渲染的页面中,需要获取一些数据,然后在客户端再进行渲染。这时候,你可能会想到使用 Redux 来管理这些数据。但是,Redux 的状态是在客户端和服务端之间共享的,这就意味着你需要在服务端和客户端之间传递这些数据。这就会导致一些问题,比如:

  • 客户端和服务端之间的状态不一致会导致一些问题...
  • 客户端和服务端之间的状态不一致会导致一些性能问题...
  • 客户端和服务端之间的状态不一致会导致一些安全问题...
  • 客户端和服务端之间的状态不一致会导致一些测试问题...
  • ...

为了解决这些问题,我们可以使用 Redux 的数据脱水。数据脱水是指将 Redux 的状态从服务端传递到客户端。这样,客户端就可以使用这些状态来渲染页面。

接下来就简单介绍一下数据脱水的实现方式。

数据脱水的实现方式有很多种,这里我就介绍一下我自己调研到的一些方式。

JSON.stringify + 手动转义(轻量基础版)

‌ 适用场景 ‌:数据简单(无复杂类型)、安全性要求不高或已做过滤 这种方式最简单,优缺点也比较明显

// 服务端渲染(需手动转义)
const data = { text: 'Hello</script>' };
const jsonString = JSON.stringify(data)
  .replace(/</g, '\\u003C')  // 转义 <
  .replace(/>/g, '\\u003E'); // 转义 >

res.send(`<script>window.__DATA__ = ${jsonString};</script>`);

// 客户端
const data = window.__DATA__;

优点 ‌

  • 无第三方依赖
  • 适合简单数据场景

缺点 ‌

  • 手动转义不全面(如未处理 等)无法处理复杂类型(如 Date、RegExp、undefined)
  • 代码冗余(需自行维护转义规则)

HTML 实体编码(如 he 库)

适用场景 ‌:对安全性要求极高,允许数据作为纯字符串处理。

import he from 'he';

// 服务端
const data = { html: '<div>Safe Content</div>' };
const encodedJSON = he.encode(JSON.stringify(data));

res.send(`
  <script>
    window.__DATA__ = JSON.parse('${encodedJSON}');
  </script>
`);

// 客户端自动解析
const data = window.__DATA__;

‌ 优点 ‌:

  • 彻底防御 XSS‌(强制所有字符转义)
  • 兼容所有数据类型(最终以字符串传递)

‌ 缺点 ‌:

  • 性能损耗 ‌(编码/解码额外开销)
  • 数据需两次解析 ‌(JSON.parse + 类型恢复)

SuperJSON(全栈类型保留)

适用场景 ‌:全栈 TypeScript 项目,需无缝保留数据类型。

// 服务端(Node.js)
import SuperJSON from 'superjson';

const data = { date: new Date(), map: new Map([['key', 'value']]) };
const serialized = SuperJSON.stringify(data);

res.send(`
  <script>
    window.__DATA__ = ${serialized};
  </script>
`);

// 客户端
import SuperJSON from 'superjson';
const data = SuperJSON.parse(window.__DATA__); // { date: Date, map: Map }

优点 ‌:

  • 完美保留类型 ‌(支持绝大多数 JS 类型)
  • 无缝集成 ‌(全栈统一序列化协议)

缺点 ‌:

  • 需客户端引入 SuperJSON‌(增加包体积)

内联

适用场景 ‌:追求安全性且无需复杂类型的场景。

<!-- 服务端 -->
<script id="__DATA__" type="application/json">
  {{ { user: "Alice", score: 95 } | json }}
</script>

<!-- 客户端 -->
<script>
  const data = JSON.parse(document.getElementById('__DATA__').textContent);
</script>

优点 ‌:

  • 天然防 XSS‌(浏览器不会解析 <script type="application/json"> 内的 HTML
  • 无依赖

缺点 ‌:

  • 需模板引擎支持 ‌(如 Handlebarsjson Helper
  • 无法处理复杂类型

我自己使用的方案 serialize-javascript

serialize 函数的作用是将 JavaScript 对象转换为一个字符串,这个字符串可以被安全地插入到 HTML 页面中,并且在客户端可以被反序列化为原始的 JavaScript 对象。 这个过程涉及到将对象中的函数和特殊字符进行转义,以防止潜在的安全漏洞,例如跨站脚本攻击(XSS)。 在服务器端渲染(SSR)的上下文中,serialize 函数通常用于将 Redux store 的初始状态转换为字符串,然后将这个字符串嵌入到 HTML 页面中。 这样,当客户端接收到 HTML 页面时,它可以使用这个字符串来初始化客户端的 Redux store,从而确保客户端和服务器端的状态保持一致。 总结一下,serialize 函数的作用是将 JavaScript 对象序列化为安全的 JSON 字符串,以便在服务器端渲染的过程中,可以将 Redux store 的初始状态嵌入到 HTML 页面中,并且在客户端能够安全地反序列化这个状态。

serialize 主要有以下6点好处
  1. 防止 XSS 攻击(安全性)

问题:

直接将 JavaScript 对象通过字符串拼接插入 HTML 如 <script>window.__DATA__ = ${data}</script>)时, 如果对象中包含用户控制的危险字符(如 </script>、'、" 等),可能导致 HTML 结构被破坏,甚至引发 XSS 漏洞。

解决‌:

serialize-javascript 会自动对特殊字符进行转义

  1. 处理复杂 JavaScript 数据类型

问题:

JSON.stringify 无法正确处理某些 JavaScript 特有类型(如 undefined、Date、RegExp、function等)

// (信息丢失)
JSON.stringify({ a: undefined, b: /regex/, c: new Date() });
//输出:{"b":{},"c":"2023-10-01T12:00:00.000Z"} 

解决:

serialize-javascript 保留类型信息,确保反序列化时数据一致性:

serialize({ a: undefined, b: /regex/, c: new Date() }); 
// 输出:{"a":undefined,"b":/regex/,"c":new Date("2023-10-01T12:00:00.000Z")}
  1. 优化客户端数据加载性能

问题:

使用 JSON.parse 解析字符串化的数据会增加客户端解析成本(尤其在大型数据集时)。

解决:

serialize-javascript 生成的字符串是可直接执行的 JavaScript 代码,客户端无需二次解析:

<script>
  window.__DATA__ = ${serialize(data)}; // 直接赋值,而非 JSON.parse
</script>
  1. 避免 HTML 注入副作用

问题‌: 未转义的字符串可能意外匹配 HTML 标签或注释语法(如 <!-- 或 -->),导致页面渲染异常。

解决‌:

serialize-javascript 对类似字符进行 Unicode 转义,确保数据仅被视为文本内容,而非 HTML 结构。

  1. 一致性

通过将 Redux store 的初始状态序列化为字符串,可以确保客户端和服务器端的状态保持一致,从而避免了客户端和服务器端状态不一致的问题。

  1. 性能: 由于 serialize 函数的执行时间相对较短,因此可以在服务器端渲染的过程中尽可能地减少对性能的影响。这对于大型应用程序来说尤为重要,因为服务器端渲染的过程通常需要处理大量的请求和数据。

几种方式的对比:

方案安全性数据类型支持性能依赖复杂度适用场景
serialize-javascript⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐通用安全传输
JSON.stringify+转义⭐⭐⭐⭐⭐⭐简单数据快速实现
he 库⭐⭐⭐⭐⭐⭐(仅字符串)⭐⭐⭐⭐高安全文本传输
SuperJSON⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐全栈 TypeScript 项目
内联 JSON Script⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐安全简单数据传输

结语

如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。

文章如有错误之处,希望在评论区指正🙏🙏