水合比较器
目标
构建一个比较器:把服务端渲染的树与客户端渲染的树进行对比,找出不一致的位置,并把它们转换成需要重绘到哪个边界为止。
为什么重要
水合是给已经存在的 DOM 附加处理函数,所以两端渲染出的内容一旦不同,页面就会悄无声息地一直错下去。因此各框架都花了很大力气去找出并报告不一致的位置。这种提示要真正有用,需要两点。
第一,结构对不上的位置必须停止比较。如果列表里有一项插到了前面,它后面的内容就会整体错位;继续往下比较会涌出几百条不一致,真正的原因反而被淹没。第二,必须知道边界。同样的不一致,在边界内部只需重绘其子树,而在边界之外则要重绘整个页面。
树与路径的结构
节点只有两种。
- 字符串:文本节点
{ tag, props, children }:元素。开启边界的节点还会多带一个boundary: "이름"
路径的根是 "$",第 i 个子节点的路径是父路径 + "/" + i。例如:根的第二个子节点的第一个子节点是 "$/1/0"。
一条不一致是 { path, kind },其中 kind 是 text、attr、tag、children 之一。如果是 attr,就在 name 中再放入属性名称。
步骤
- 文本比较与路径
- 属性比较:顺序不算不一致
- 结构对不上就停止
- 抑制只作用一层
- 附上最近的边界
- 计算需要重绘的位置:
repairPlan - 亲自测量并记录:
/root/work/hydrate/07-report.txt - 总结:
/root/work/hydrate/08-notes.md
参考
- 第 7 步读取的那一对树放在镜像里:
/opt/fixtures/ssr-hydrate/pair.json(包含server和client两个键)。 - 评分器会 import 你的
/root/work/hydrate/hydrate.mjs,只检查契约。不使用浏览器,也不使用框架。 - 两个常见错误:把属性顺序算作不一致;以及子节点数量不同却继续往下比较,导致不一致数量被放大。
文本比较与路径
在 /root/work/hydrate/hydrate.mjs 中 export diff(server, client)。对每个不一致的位置,返回一个包含 {path, kind} 的数组。文本不同时 kind 为 "text",只有一边是文本时为 "tag"。两棵树相同则返回空数组。
mkdir -p /root/work/hydrate。路径的根是 "$",第 i 个子节点的路径是在父路径后面加上 "/" + i。递归向下时,把路径作为参数带着走。这一步可以认为两棵树的结构相同。
属性比较:顺序不算不一致
比较元素节点的 props。值不同,或者只在一边存在,就放入 {path, kind: "attr", name}。属性的顺序不算不一致。
把两边的键收集成一个集合,再按名称逐个比较值,顺序自然就不起作用了。只在一边存在的属性,另一边的值是 undefined,所以会被自动检出。浏览器并不靠顺序区分属性,如果把顺序也算作不一致,完全正常的页面会被标红一大片。
结构对不上就停止
标签不同时,放入一条 {path, kind: "tag"},并且不要再往它下面比较。子节点数量不同时,放入一条 {path, kind: "children"},同样停止。
如果列表里有一项插到了前面,后面的内容会全部错位。继续往下比较,就会拿并不对应的节点互相比较,涌出几百条不一致,真正的那一个原因被埋在里面。评分器会用错开一格的列表来检查这一点。
抑制只作用一层
让 diff(server, client, options) 的 options.suppress 接收一个路径数组。只排除该路径自身的 text 和 attr 不一致,它下面的路径仍然照常报告。tag 和 children 不在抑制范围内。
没有第三个参数,或者传入空对象时,也必须正常工作。只允许抑制一层,原因和 React 的 suppressHydrationWarning 相同:为了放过某一处时间而对它下面也视而不见,里面真正的不一致就永远看不到了。
附上最近的边界
给每个不一致再放入 boundary。它是服务端树中包住该路径的最近的 boundary 值,没有包住它的边界时为 null。如果节点自己开启了边界,该节点的不一致也属于它自己的边界。
遍历一遍树,为每个路径记录“当前开启的边界”,之后只需要查询即可。嵌套的边界中,内层优先。出现 null 与否,就是设计的成绩单:只要有那一个,整个页面都会被重绘。
计算需要重绘的位置
export repairPlan(mismatches)。它返回 {boundaries, full}。boundaries 是需要重绘的边界 id 数组,不重复,按首次出现的顺序排列;只要存在一个边界之外的不一致,full 就为真。
同一个边界内即使出现十个不一致,重绘也只有一次。所以答案是集合而不是列表,但保持便于人阅读的顺序会更好。边界之外的不一致不要放进 boundaries,只用 full 来表示。
亲自测量并记录
把 /opt/fixtures/ssr-hydrate/pair.json 的 server 和 client 分别放进你的 diff 和 repairPlan,把得到的值以 이름=값(占位符依次为名称与值)的形式写成三行,保存到 /root/work/hydrate/07-report.txt。mismatches 是不一致的数量,boundaries 是需要重绘的边界数量,full 在存在边界之外的不一致时为 yes,否则为 no。
写一个简短的 .mjs 脚本运行一下,把得到的值抄下来即可。评分器会用你的比较器把同一个文件重新比较一遍,并与你写的值核对,所以不要靠眼睛估算。如果在子节点数量不同的位置没有停止,不一致的数量就会变多。
是什么把不一致限制在局部
在 /root/work/hydrate/08-notes.md 中写至少三行:服务端和客户端渲染出不同内容时页面上会看到什么;边界之外的不一致意味着什么;为什么抑制只允许一层。
正文里必须出现 불일치、경계、억제(韩文,依次意为“不一致”“边界”“抑制”)。这三样,是使用 SSR 的团队实际要花上好几天去处理的三件事。