From 824e165ede55f93c847fa7c46a0618dd547e36c6 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Wed, 1 Apr 2026 12:00:00 +0800 Subject: [PATCH] add internal design docs --- README.md | 12 +++- docs/observer.md | 122 +++++++++++++++++++++++++++++++++++++++++ docs/replay.md | 48 ++++++++++++++++ docs/serialization.md | 125 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 docs/observer.md create mode 100644 docs/replay.md create mode 100644 docs/serialization.md diff --git a/README.md b/README.md index 602965b9..08922464 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,18 @@ rrweb 主要由 3 部分组成: - 补充更多单元测试 - 随机在更多网站上运行集成测试 +## Internal Design + +[序列化](./docs/serialization.md) +[增量快照](./docs/observer.md) +[回放](./docs/replay.md) + ## Contribute Guide +为了保证录制和回放时可以对应到一致的数据结构,rrweb 采用 typescript 开发以提供更强的类型支持。 + +[Typescript 手册](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) + 1. Fork 需要修改的 rrweb 组件仓库 2. `npm install` 安装所需依赖 3. 修改代码并通过测试 @@ -43,7 +53,7 @@ rrweb 主要由 3 部分组成: 除了添加集成测试和单元测试之外,rrweb 还提供了交互式的测试工具。 -运行 `npm rnu repl`,将会启动浏览器并在命令行要求输入一个测试的 url: +运行 `npm run repl`,将会启动浏览器并在命令行要求输入一个测试的 url: ``` Enter the url you want to record, e.g https://react-redux.realworld.io: diff --git a/docs/observer.md b/docs/observer.md new file mode 100644 index 00000000..4b1f8525 --- /dev/null +++ b/docs/observer.md @@ -0,0 +1,122 @@ +# 增量快照 + +在完成一次全量快照之后,我们就需要基于当前视图状态观察所有可能对视图造成改动的事件,在 rrweb 中我们已经观察了以下事件(将不断增加): + +- DOM 变动 + - 节点创建、销毁 + - 节点属性变化 + - 文本变化 +- 鼠标移动 +- 鼠标交互 + - mouse up、mouse down + - click、double click、context menu + - focus、blur + - touch start、touch move、touch end +- 页面或元素滚动 +- 视窗大小改变 +- 输入 + +## Mutation Observer + +由于我们在回放时不会执行所有的 JavaScript 脚本,所以例如这样的场景我们需要完整记录才能够实现回放: + +> 点击 button,出现 dropdown menu,选择第一项,dropdown menu 消失 + +回放时,在“点击 button”执行之后 dropdown menu 不会自动出现,因为已经没有 JavaScript 脚本为我们执行这件事,所以我们需要记录 dropdown menu 相关的 DOM 节点的创建以及后续的销毁,这也是录制中的最大难点。 + +好在现代浏览器已经给我们提供了非常强大的 API —— [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) 用来完成这一功能。 + +此处我们不具体讲解 MutationObserver 的基本使用方式,只专注于在 rrweb 中我们需要做哪些特殊处理。 + +首先要了解 MutationObserver 的触发方式为**批量异步**回调,具体来说就是会在一系列 DOM 变化发生之后将这些变化一次性回调,传出的是一个 mutation 记录数组。 + +这一机制在常规使用时不会有问题,因为从 mutation 记录中我们可以获取到变更节点的 JS 对象,可以做很多等值比较以及访问父子、兄弟节点等操作来保证我们可以精准回放一条 mutation 记录。 + +但是在 rrweb 中由于我们有序列化的过程,我们就需要更多精细的判断来应对各种场景。 + +### 新增节点 + +例如以下两种操作会生成相同的 DOM 结构,但是产生不同的 mutation 记录: + +``` +body + n1 + n2 +``` + +1. 创建节点 n1 并 append 在 body 中,再创建节点 n2 并 append 在 n1 中。 +2. 创建节点 n1、n2,将 n2 append 在 n1 中,再将 n1 append 在 body 中。 + +第 1 种情况将产生两条 mutation 记录,分别为增加节点 n1 和增加节点 n2;第 2 种情况则只会产生一条 mutation 记录,即增加节点 n1。 + +**注意**,在第一种情况下虽然 n1 append 时还没有子节点,但是由于上述的批量异步回调机制,当我们处理 mutation 记录时获取到的 n1 是已经有子节点 n2 的状态。 + +受第二种情况的限制,我们在处理新增节点时必须遍历其所有子孙节点,才能保证所有新增节点都被记录,但是这一策略应用在第一种情况中就会导致 n2 被作为新增节点记录两次,回放时就会产生与原页面不一致的 DOM 结构。 + +因此,在处理一次回调中的多个 mutation 记录时我们需要“惰性”处理新增节点,即在遍历每条 mutation 记录遇到新增节点时先收集,再在全部 mutation 遍历完毕之后对收集的新增节点进行去重操作,保证不遗漏节点的同时每个节点只被记录一次。 + +在[序列化设计](./serialization.md)中已经介绍了我们需要维护一个 `id -> Node` 的映射,因此当出现新增节点时,我们需要将新节点序列化并加入映射中。但由于我们为了去重新增节点,选择在所有 mutation 记录遍历完毕之后才进行序列化,在以下示例中就会出现问题: + +1. mutation 记录1,新增节点 n1。我们暂不处理,等待最终去重后序列化。 +2. mutation 记录2,n1 新增属性 a1。我们试图将它记录成一次增量快照,但会发现无法从映射中找到 n1 对应的 id,因为此时它还未被序列化。 + +由此可见,由于我们对新增节点进行了延迟序列化的处理,所有 mutation 记录也都需要先收集,再新增节点去重并序列化之后再做处理。 + +### 移除节点 + +在处理移除节点时,我们需要做以下处理: + +1. 移除的节点还未被序列化,则说明是在本次 callback 中新增的节点,无需记录,并且从新增节点池中将其移除。 +2. 上步中在一次 callback 中被新增又移除的节点我们将其称为 dropped node,用于最终处理新增节点时判断节点的父节点是否已经 drop。 + +### 属性变化覆盖写 + +尽管 MutationObserver 是异步批量回调,但是我们仍然可以认为在一次回调中发生的 mutations 之间时间间隔极短,因此在记录 DOM 属性变化时我们可以通过覆盖写的方式优化增量快照的体积。 + +例如对一个 `