跳到正文
原文
Make WordPress Core(核心开发)· alecgeatches·· 15 天前编辑评分56

WordPress 实时协作同步引擎测试指南:对比 yjs-server、DE-RTC 与 intent-log

Real-time Collaboration: A Guide to Sync Engines

编辑导读

WordPress 核心开发团队通过实验插件 gutenberg-sync-engines 提供 yjs-server、DE-RTC 和 intent-log 三种实时协作同步引擎,供测试者评估后续服务端方案。

在转向采用服务器感知模式的协作机制时,@chriszarate 解释了为什么服务器为何应成为实时协作的核心,并介绍了我们一直在研究、用于为 RTC 未来提供支持的各个引擎。本文将探讨为 RTC 构建的每种同步引擎,以及它们实现协作的不同方式。

目前,所有引擎都位于实验性插件 gutenberg-sync-engines 中,你 现在就可以运行它来评估下面的各个引擎。我们的目标是在你的帮助下评估引擎选项,并为 WordPress 服务端 RTC 选定下一套引擎基础。

本文既是一份测试指南,也是对每种引擎相关概念的介绍。我们希望你能试用每种引擎,并帮助我们评估哪一种最符合 WordPress 中 RTC 的理想未来。

术语

以下是本文中用到的一些值得解释的常见技术术语:

  • 同步引擎是在多人编辑同一篇文章时用于合并改动的逻辑。gutenberg-sync-engines 中提供的三种同步引擎是 yjs-server、DE-RTC 和 intent-log。
  • 传输方式是改动在浏览器与服务器之间传播的途径,包括 HTTP 轮询、 WebRTC、WebSockets 和服务器发送事件(SSE)的组合。任何引擎都可搭配任何传输方式使用。
  • 任何被暂缓以供审查的改动都会被保存在某处,但不会应用到文章中。这可能适用于合并冲突,以及因安全原因而被阻止的改动。当改动被暂缓以供审查时,需要由人员决定是否接受。

设置

可在 Settings → Collaboration 下选择同步引擎和传输方式:

运行该插件需要 WordPress 7.0+ 和 Presence API 插件,后者用于跟踪哪些用户正在查看哪篇文章。该插件自带一份内部(经轻微修改的)Gutenberg 副本,以便让同步引擎可替换,因此它无法与其他正在运行的 Gutenberg 插件一起使用。

如需试用这些引擎,请按照 README 中的设置步骤操作。选择一种同步引擎和传输方式,然后在多个浏览器中开始编辑一篇文章。

引擎:它们有哪些共同点?

在所有引擎中,每项改动都会经过 WordPress 处理。当收到一项编辑时,服务器会确认发送者身份,检查其是否有权进行该编辑,验证其中是否含有不安全代码,确定如何将该编辑与其他人的改动合并,并存储结果。各引擎之间的差异在于这些步骤的实现方式。每种引擎都允许多个用户同时编辑,支持所有传输方式,并以不同程度显示谁正在编辑文章。

引擎 1:yjs-server

摘要:该引擎基于无冲突复制数据类型(CRDT)库 Yjs 构建,而 Yjs 也是当前 Gutenberg 实验中用于客户端 RTC 的技术。每个浏览器都使用 Yjs 库将改动合并到文档中,并把结果发送到服务器。服务器会在将改动共享给其他用户之前进行验证。

Yjs 是一种开源且广泛使用的 CRDT 实现。在 Yjs 中,分别编辑的同一文档的两个副本始终可以按任意顺序合并,并且最终两者完全一致,不会发生合并冲突。服务器实现会运行 PHP 版本的 Yjs,在将改动传递给其他用户之前进行验证。

引擎:yjs-server
传输方式:轮询,并使用 WebRTC 辅助通道

你的更改如何传递给协作者:你输入一个词。你的浏览器会在编辑器文档的本地副本上应用该词,并向服务器发送二进制更新。服务器打开文章的共享副本,验证更新并保存。其他所有人都会通过引擎传输收到该更新,并将其应用到文档的本地副本。

优势

  • 经过最充分实战检验的协作库。Yjs 被许多产品使用,并且在 Gutenberg 中也接受了最充分的测试。
  • 没有合并冲突。Yjs 采用确定性算法来合并编辑,因此不会出现与文档冲突的编辑。
  • 最接近“实时”的协作体验。Yjs 更新快速且体积小(无论文档大小如何),能够传递光标级在线状态,并旨在提供实时协作体验。

劣势

  • Yjs 会自行选择胜者,从而“隐藏”冲突。如果两名协作者同时更改某个区块设置,其中一名用户会悄无声息地胜出。如果两个人同时重写同一个句子,结果可能是两个人都没有单独写过的混合内容。由于引擎不将这些视为冲突,因此不会有人收到冲突警告。
  • 它使用压缩的二进制消息进行通信。Yjs 消息不是“用户 A 在第 1 段添加了‘Lorem ipsum’”,而是形如 { "type": "update", "data": "AAAG+/m3og8CA4IERAE0A6gAx…" }。这有利于减小更新体积,但可能使调试和理解历史记录更加困难。
  • 服务器代理无法轻松参与协作。直接修改文章内容的外部工具很难与实时协作进行交互,这可能导致编辑器的工作副本与数据库中的文章内容发生偏离。
  • 存在两套实现。yjs-server 引擎需要配套的 JS 和 PHP 实现,因为浏览器负责合并更改,而服务器还必须进行验证。两套实现通过共享测试保持一致,但二者一旦出现偏离就会引发问题。协议更新必须同时在服务器端和客户端进行。

引擎 2:DE-RTC

摘要:它与单用户 WordPress 最为相似。人们仅在保存时共享和合并更改。服务器会将传入的保存与其他人的保存合并,并在内容重叠时产生合并冲突。此引擎并非为 Google Docs 风格的实时编辑而设计,而是采用较慢的更新节奏。由于更改通过保存完成,因此外部插件、脚本或修改文章内容的 WP-CLI 命令几乎可以像人工编辑一样轻松地参与其中。

DE-RTC 中的“DE”代表 Distributed Editing。@dmsnell 在 WordPress/wordpress-develop#12334 和 WordPress/gutenberg#79602 中提出了这一设计,我们已将其移植到同步引擎插件中。它遵循一种更传统的 WordPress 模型:如果保存是人们共享工作的方式,那么任何能够保存文章的程序都可以参与,包括非浏览器工具。尽管 DE-RTC 可以通过每种传输方式发送辅助数据,但文章数据更新仍按照配置的保存节奏,通过常规保存来发送。

引擎:DE-RTC,保存节奏为 10 秒

你的更改如何传递给协作者:你进行一些更改。每隔十秒(默认设置),你的浏览器会通过 WordPress 的常规自动保存机制,将整篇文章发送给服务器。服务器会基于文章的三份副本来合并:你开始编辑时的版本、你更新后的版本,以及服务器上当前的共享版本。不重叠的更改会自动合并。当两个人以相互冲突的方式修改某个区块时,该区块会被暂时搁置,等待审核。

引擎:DE-RTC,保存间隔为 10 秒。合并对话框的改进工作正在进行中。

优势

  • 外部工具可以轻松参与。能够修改文章内容的插件、脚本或智能体可以与实时编辑器协同工作,而不是替换编辑器中已有的工作。
  • 只需一套合并实现,由服务器负责。浏览器端逻辑很简单,只需将整篇文章发送给服务器,由服务器负责合并。与 yjs-server 和 intent-log 不同,无需在浏览器和服务器之间重复实现逻辑。
  • 非常适合规模小、成本低的托管环境。在较长间隔内发起少量较大的请求,通常比持续轮询对资源受限的托管主机更友好。

劣势

  • 它并非真正实时。你会在保存间隔到来时看到其他人的更改,而不是带有实时更新的作者光标。保存间隔可以调整,但 DE-RTC 最适合更新频率较低的场景。
  • 每次保存都会发送整篇文章,因此文章越长,更新内容就越大。
  • 外部工具必须说明其基于哪个版本开始编辑。为了让外部文章编辑能与编辑器中的其他人合并,工具必须说明其开始编辑时所依据的文章版本(例如 Base-version: 123)。这比其他引擎更简单,但要求工具了解 RTC。
  • 服务器不知道你的真实意图。DE-RTC 只能看到保存前后的内容,无法得知“我拆分了这个段落”之类的意图。一起进行的大幅文章更改,例如拆分或重新排列区块,更常被暂时搁置,等待审核。

引擎 3:intent-log

概述:浏览器会发送描述用户在 Gutenberg 中执行了哪些操作的消息,服务器负责维护日志。例如:“在索引 20 处拆分区块 1”,然后“在区块 2 中添加文本‘Lorem Ipsum’”。这些消息以人类可理解的方式说明更改。并行更改通过操作变换进行合并。不兼容的更改会像 DE-RTC 一样引发冲突。

引擎:intent-log
传输方式:轮询(间隔为 10 秒,以便留出编辑时间)

你的更改如何传递给协作者:你输入一些文本。浏览器会描述这一操作:“在区块 1 的位置 0 插入‘Lorem ipsum’”,并将该消息发送给服务器。服务器会根据此后到达的任何更改调整你的更改(如果协作者在你的内容前添加了三个字母,你的插入位置就会向后移动三个位置),将其加入日志,然后返回答复。冲突会被暂时搁置,交由人工合并审核。你的更改处理完毕后,其他所有人都会收到服务器端正式的操作日志。

上述演示对应的 intent-log 消息示例:

// User B adds content to block 02f5e6c3
{
    "intentId": "...",
    "baseSeq": 585,
    "type": "insert_text",
    "payload":{
        "syncId": "02f5e6c3-2876-43a6-b041-6f3d0a217095",
        "field": "content",
        "offset": 42,
        "text": " in the editor."
    }
}

// At the same time, User A merges block 02f5e6c3 into block 8d979605
{
    "intentId": "...",
    "baseSeq": 585,
    "type": "merge_blocks",
    "payload":{
        "survivorId": "8d979605-e1d7-4cec-bc6e-4a3eaa6f30bc",
        "absorbedId": "02f5e6c3-2876-43a6-b041-6f3d0a217095",
        "field": "content",
        "joinOffset": 48
    }
}

优势

  • 服务器能以人类可理解的方式了解发生了什么。在 yjs-server 中,在文章里移动一个区块可能看起来像整篇文章被重写,因为引擎在比较更改时缺乏上下文。而在 intent-log 中,移动区块只是一条简单的消息(“将区块 1 移动到位置 3”)。这支持上下文感知的合并策略,以及服务器对更改的理解。
  • Intent-log 会保留一份可供人类阅读的会话记录。 每篇文章都内置了最近操作及其执行者的可读历史记录。
  • 外部工具可以加入,只需说明其起始版本,方式类似 DE-RTC。外部保存的内容可以转换为 intent-log 编辑,并与其他编辑合并。

弱点

  • Intent-log 每篇文章只能一次处理一项更新。 Intent-log 使用锁来逐一管理传入的消息。在单篇文章中大量使用时,锁争用可能成为问题,导致更新同步速度变慢。
  • 操作变换很难实现。Intent-log 建立在操作变换之上,需要重新调整并发编辑的位置,使其能够在无冲突的情况下合并。这项棘手的逻辑在 intent-log 中仍需大量完善。
  • 两种实现。intent-log 引擎同时需要 JS 和 PHP 实现,因为浏览器既要即时显示你的编辑,也要在其他更改到达时将其保留在正确位置。这意味着 intent-log 会在浏览器(JS)和服务器(PHP)上运行相同的逻辑,以避免两份日志发生分离。通过共享测试来保持两种实现一致,但二者一旦出现偏差就会引发问题。协议更新必须同时在服务器端和客户端完成。

并排比较

yjs-serverDE-RTCintent-log
更新到得快吗?是否是
文本中的实时光标是,Google Docs 风格否是(尚未实现)
浏览器发送什么二进制更新整篇文章编辑内容的简短描述
合并逻辑位于何处?服务器和浏览器服务器服务器(带浏览器预览)
两个人做出冲突更改静默合并,由一人胜出暂存以供审查暂存以供审查
外部工具不与实时会话合并通过指定版本加入通过指定版本加入
每次更改的服务器工作量大部分,随文章大小增长中等,上传量随文章大小增长最少

传输方式:更改如何传输

本文主要关注引擎选择,但我们有多种可供所有引擎支持的传输方式。我们预计传输选项将取决于托管服务的可用情况。以下是对这些传输方式的简要介绍:

传输方式工作原理所需条件
轮询浏览器发送本地更新,并按定时器向服务器请求更新,每 N 秒一次。可调整为 1 至 25 秒之间的任意值(默认 5 秒)。无需特殊基础设施。可在任何主机上运行,代价是扩展时需要更多 PHP 工作进程。
使用 WebRTC 通知通道的轮询浏览器发送本地更新,并按较为宽松的轮询定时器向服务器请求更新。不过,当任意对等方做出更改时,其他对等方会通过 WebRTC 收到通知,立即向服务器请求更新,而不必等到下一个轮询周期。浏览器能够通过 WebRTC 相互连接。无法实现时,回退到常规轮询。
服务器发送事件(Server-sent events)浏览器与 WordPress 保持一个连接持续开启,并通过该连接接收更新。本地更新发送到轮询端点。需要支持长时间保持连接的托管服务。每个打开的窗口需要一个服务器进程,可能会占用 PHP 工作进程。
服务器发送事件(守护进程)与服务器发送事件使用相同的单向连接,但运行在稳定的独立进程上,而不是通用 PHP 工作进程中。本地更新发送到轮询端点。一台能够运行独立进程的服务器,以及支持长时间保持连接的托管服务。
WebSocket双向保持开放的连接,在独立进程中运行。变更几乎会即时到达。一台能够运行独立 WebSocket 进程的服务器。

保真度更高的传输方式在设计上支持降级,并可回退到轮询。这意味着,如果你选择的传输方式依赖独立服务(例如 WebSocket 守护进程),而该服务停止响应,轮询将始终可用作为备用方案。

行动号召

试用一下并告诉我们你的想法!测试时请记住,所有三个引擎都还需要完善,可能存在缺陷。我们希望听到有关缺陷的反馈,但更关注各引擎之间的主要设计差异,以及这些差异可能如何影响你的工作流。快速更新和实时光标对你来说是必需品吗?你更倾向于 DE-RTC 更简单的模型吗?你对遇到的合并冲突有何看法?

请在 GitHub issues 或 WordPress Slack 的 #feature-realtime-collaboration 中报告你的发现。有关更深入的信息,请参阅存储库中的文档:

  • data-flow.md 在一页中展示了各引擎的技术数据流。
  • engine-comparison.md 从更多方面比较了各引擎。

如果有任何地方我们能够协助改进你的测试,请告诉我们。

来源:Make WordPress Core(核心开发) · https://make.wordpress.org/core/2026/10/14/real-time-collaboration-a-guide-to-sync-engines/

部分链接为推广/关联链接。