引言:Yjs 是什么?

Yjs 是一个高性能的 JavaScript 框架,专门用于构建实时协作应用程序。 想象一下 Google Docs 或 Figma 这样的应用,多个用户可以同时在同一个文档上编辑,并且能立即看到彼此的修改。Yjs 正是实现这类功能的神器。

它解决的核心问题包括:

  • 实时协作:允许多个用户无缝地同时编辑数据。
  • 离线编辑:用户在网络断开时仍然可以进行修改,并在重新连接后自动同步。
  • 数据同步:确保所有客户端的数据最终达到一致状态。

这一切都得益于其底层的 CRDT (Conflict-free Replicated Data Type,无冲突复制数据类型) 算法。简单来说,CRDT 允许数据在不同副本上独立修改,并且总能以一种可预测、无冲突的方式合并,从而避免了复杂的冲突解决逻辑。

Yjs 的三大核心概念

要掌握 Yjs,首先需要理解它的三个基本构建块。

  1. Y.Doc (文档)
    Y.Doc 是 Yjs 的核心,它就像一个项目的“容器”或“数据库”。所有需要共享和同步的数据都存放在一个 Y.Doc 实例中。 你可以把它想象成一个 Git 仓库,它追踪着所有的变更历史。

  2. Shared Types (共享类型)
    你不能直接在 Y.Doc 中存储原始的 JavaScript 对象。相反,你需要使用 Yjs 提供的特殊数据结构——共享类型。这些类型是 CRDT 的具体实现,确保了并发修改的正确性。 常见的共享类型有:

    • Y.Text: 用于协同编辑文本,支持富文本。
    • Y.Array: 用于协同管理列表,如待办事项清单。
    • Y.Map: 用于存储键值对,如应用配置或用户数据。
    • Y.XmlFragment: 用于协同编辑 XML/HTML 内容。
  3. Providers (连接器)
    Provider 是负责在客户端之间同步 Y.Doc 更新的“信使”。它处理网络通信,将本地的变更广播出去,并接收来自其他客户端的变更。Yjs 官方提供了多种 Provider:

    • y-websocket: 通过 WebSocket 实现客户端与服务器之间的实时同步。
    • y-webrtc: 使用 WebRTC 实现点对点 (P2P) 的去中心化同步。
    • y-indexeddb: 将文档变更持久化到浏览器的 IndexedDB,实现离线支持。

入门实践:构建一个简单的协同编辑器

理论讲完了,让我们动手写一个最简单的多人协同 <textarea>

步骤一:项目初始化

首先,你需要一个简单的后端 WebSocket 服务器来同步数据。Yjs 社区已经为我们准备好了。

# 安装 y-websocket 的服务端
npm install y-websocket
# 启动服务
npx y-websocket

然后,在你的前端项目中安装 Yjs 和 y-websocket 客户端。

npm install yjs y-websocket
步骤二:创建 Yjs 文档和共享类型

在你的 JavaScript 文件中,初始化 Y.Doc 和我们需要的 Y.Text

import * as Y from 'yjs';

// 1. 创建一个 Yjs 文档
const ydoc = new Y.Doc();

// 2. 获取一个名为 "my-text" 的 Y.Text 共享类型
// 如果这个共享类型不存在,Yjs 会自动创建它
const ytext = ydoc.getText('my-text');
步骤三:连接网络

使用 WebsocketProvider 将我们的 ydoc 连接到 WebSocket 服务器。

import { WebsocketProvider } from 'y-websocket';

// 3. 连接到 websocket 服务器
// "ws://localhost:1234" 是 y-websocket 默认启动的地址
// "my-collaboration-room" 是房间名,只有在同一个房间的客户端才会同步数据
const provider = new WebsocketProvider(
  'ws://localhost:1234', 
  'my-collaboration-room', 
  ydoc
);

连接成功后,Provider 会自动处理所有同步细节。

步骤四:双向绑定

现在,我们需要将 ytext 的内容和页面上的 <textarea> 元素进行双向绑定。

<textarea id="editor"></textarea>
const editor = document.getElementById('editor');

// 绑定编辑器 -> Yjs
editor.addEventListener('input', () => {
  // 当用户输入时,计算与 ytext 的差异并应用更新
  const currentText = editor.value;
  // ytext.delete(0, ytext.length)
  // ytext.insert(0, currentText)
  ytext.applyDelta([
    { retain: 0 },
    { delete: ytext.length },
    { insert: currentText }
  ]);
});

// 绑定 Yjs -> 编辑器
ytext.observe(event => {
  // 当 ytext 发生变化时 (来自本地或远程),更新编辑器内容
  editor.value = ytext.toString();
});

// 初始加载
window.addEventListener('load', () => {
  editor.value = ytext.toString();
});

现在,打开多个浏览器窗口访问你的页面,在一个窗口中输入内容,你会立刻看到其他窗口同步了变化!

深入了解核心 API

Shared Types (共享类型) 详解

共享类型是 Yjs 的精髓。除了 getText,还有 getArraygetMap

  • Y.Array 示例
    const yarray = ydoc.getArray('my-todo-list');
    
    // 监听数组变化
    yarray.observe(event => {
      console.log('数组发生了变化:', event.changes);
    });
    
    // 推入新项目 (这些操作会同步到其他客户端)
    yarray.push(['新任务']);
    
  • Y.Map 示例
    const ymap = ydoc.getMap('user-settings');
    
    // 监听 Map 变化
    ymap.observe(event => {
      console.log('Map 发生了变化:', event.keysChanged);
    });
    
    // 设置键值对
    ymap.set('theme', 'dark');
    
Awareness (感知) 协议:超越文档同步

文档内容同步了,但协作应用还需要同步一些临时状态,比如谁在线、他们的光标在哪里、他们正在选择什么。这就是 Awareness 的用武之地。

Awareness 数据不会被持久化,当用户关闭页面时就会消失。

  • 使用方法
    Awareness API 通过 Provider 实例访问。

    import { WebrtcProvider } from "y-webrtc";
    
    // Awareness 协议在 provider 上可用
    const awareness = provider.awareness;
    
    // 监听状态变化事件
    awareness.on('change', changes => {
      // 获取所有在线用户的状态
      const states = Array.from(awareness.getStates().values());
      console.log(states);
      // 在这里根据 states 渲染光标或在线用户列表
    });
    
    // 设置当前客户端的状态
    awareness.setLocalStateField('user', {
      name: 'Alice',
      color: '#ff0000' // 光标颜色
    });
    

    通过监听 change 事件并使用 getStates(),你就可以轻松实现一个在线用户列表或多人光标功能。

离线支持与数据持久化

Yjs 的设计天然支持离线。当网络断开时,Provider 会自动尝试重连。在此期间,用户的所有修改都会被保存在 Y.Doc 中。一旦网络恢复,积压的变更会自动同步给其他客户端。

为了实现真正的离线优先,你通常需要将文档状态保存在本地。这可以通过 y-indexeddb 实现。

import { IndexeddbPersistence } from 'y-indexeddb';

// 将 ydoc 的状态持久化到 IndexedDB
const persistence = new IndexeddbPersistence('my-document-name', ydoc);

只需这两行代码,你的应用就拥有了离线存储能力!用户下次访问时,即使没有网络,也能从 IndexedDB 中恢复上次的编辑状态。

总结:为什么选择 Yjs?

Yjs 不仅仅是一个库,它是一个完整的、经过实战检验的协同解决方案。

  • 高性能:Yjs 的更新编码非常高效,网络开销极小。
  • 生态丰富:拥有对 Prosemirror, CodeMirror, Monaco 等流行编辑器的现成绑定。
  • 去中心化:CRDT 的特性让它不依赖中央服务器来解决冲突,简化了后端逻辑。
Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐