Fresh序列化机制揭秘:Islands间传递Props的JSON编解码原理

Fresh序列化机制揭秘:Islands间传递Props的JSON编解码原理

【免费下载链接】fresh The framework so simple, you already know it. 【免费下载链接】fresh 项目地址: https://gitcode.com/gh_mirrors/fr/fresh

在 Fresh 框架中,Islands(岛屿)组件运行在浏览器,而页面的数据与 Props 诞生于服务器端。两者之间隔着一条"网络边界"——服务器如何把组件属性无损地传给客户端?答案藏在 Fresh 专门实现的 JSON 序列化机制里。本文将用通俗的方式,带你快速看懂这套编解码原理,以及它如何让 Signal、日期、循环引用等特殊数据都能安全"过桥"。

Fresh框架Islands间Props传递的JSON序列化机制概览

为什么原生 JSON.stringify 不够用?

🤔 先想一个问题:JSON.stringify 是我们最常用的序列化工具,Fresh 直接用它不行吗?

其实不行。原生 JSON 在以下场景会"掉链子":

数据类型JSON.stringify 的表现Fresh jsonify 的表现
undefined字段直接消失保留为占位标记
Date变成字符串,还原不了可还原为 Date 对象
NaN / Infinity变成 null各自有专属占位符
Set / Map变成 {}完整保留键值
循环引用直接抛错支持,同一对象只编码一次

Fresh 的解决方案是内置一个独立的 jsonify 模块,位于 jsonify/ 目录,核心就三个文件:

配套还有 round_trip_test.ts 等测试文件,验证"编码→解码"往返后数据完全一致。

编码原理:扁平数组 + 索引引用 🔁

Fresh 序列化后的产物看起来像一个数组,例如 [{"name":"Fresh","count":1}]。它有两个关键设计:

1️⃣ 负数索引当"特殊值占位符"

原始数字和负数不会同时出现在同一层面,于是 Fresh 用负数编码"无法用 JSON 表达"的值(见 constants.ts):

  • -1undefined
  • -2null
  • -3NaN
  • -4 / -5 → 正负无穷
  • -6 → 负零
  • -7 → 数组"空洞"

2️⃣ 每个唯一值只编码一次,之后用索引引用

编码时维护一张"已见对象"索引表:对象第一次出现时被完整写入,再次出现(哪怕在完全不同的位置)只写入一个数字索引。这个设计带来两个好处:

  • 省空间:同一份数据在多个 Props 间共享时不重复传输
  • 🔄 天然支持循环引用:引用先于定义出现也没关系,解码时再回溯

对于 DateURLRegExpBigIntUint8Array(Base64 编码)等结构化类型,则使用"标签数组"格式,如 ["Date","2026-01-01T00:00:00.000Z"],解码端按第一个字符串标签分派还原逻辑。

服务端:Props 如何嵌入 HTML 🚚

真正触发序列化的是 SSR 渲染阶段。在 preact_hooks.ts 中,服务器做三件事:

  1. 收集:渲染页面时,把每个岛屿的 Props 依次压入 islandProps 数组
  2. 打标:给每个岛屿的 HTML 前后包裹注释标记 frsh:island:岛屿名:propsIdx:key,其中的 propsIdx 就是该岛屿 Props 在数组里的下标
  3. 注入:调用 stringify(islandProps, stringifiers) 把整个 Props 数组压成一段紧凑字符串,随内联脚本 boot(...) 一起写入 HTML

这里还注册了自定义字符串化器,处理框架专属类型:

  • Signal → 序列化为它当前的值(调用 peek()
  • Computed → 同样取当前值
  • Slot → 序列化为 {name, id} 的轻量描述,而不是整个虚拟节点

⚠️ 注意:函数(Function)无法序列化,传入会被明确报错——这是所有 SSR 框架的共同边界。

对于局部更新(Partial)场景,序列化结果会被写进独立的 <script type="application/json"> 标签中,结构如下图所示的"分区替换"思路一致:

Fresh框架Partial局部刷新与Props注入示意图

客户端:一键复活 Props ⚡

浏览器加载页面后,客户端引导代码 reviver.tsboot() 函数接管:

  1. 扫描 DOM,找到所有 frsh:island:... 注释标记,读出岛屿名和 propsIdx
  2. 解码:用 parse(字符串, CUSTOM_PARSER) 把序列化的 Props 还原成完整的 JS 对象
  3. 重建响应式:解码时通过自定义解析器"复活"特殊类型——Signal 重新变成 signal()Computed 变成 computed()Slot 变成指向 DOM 片段的引用
  4. 渲染:把还原好的 Props 交给岛屿组件,完成激活

也就是说,你在服务端传给岛屿的 count Signal,客户端拿到的还是一个真正可响应更新的 Signal,而不是死掉的数字。

实战要点清单 ✅

场景建议
传递函数❌ 不可行,请改为传数据
传递 Signal / Computed✅ 自动取当前值并在客户端重建
多个岛屿共享同一对象✅ 自动去重,只传输一份
传递 Date、URL、Set、Map✅ 原生支持,无需转换
传递 Uint8Array✅ 自动 Base64 编解码

想亲手验证编解码行为,可以阅读 custom_test.ts 中的自定义类型示例;更多岛屿相关概念可参考官方文档 docs/latest/concepts/islands.mddocs/latest/advanced/serialization.md

总结

Fresh 的序列化机制看似只是"换了一个 JSON 编码器",实则是岛屿架构的隐形基石:扁平数组加索引引用解决了去重与循环引用,负数占位符补齐了 JSON 的类型盲区,标签数组让结构化数据无损往返,自定义字符串化器则把框架特有的 Signal 与 Slot 也纳入传输范围。理解这套编解码原理后,你就能明白为什么在 Fresh 中"往岛屿里传什么"比想象中自由得多——也更能写出高性能的服务端渲染应用。

【免费下载链接】fresh The framework so simple, you already know it. 【免费下载链接】fresh 项目地址: https://gitcode.com/gh_mirrors/fr/fresh

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值