协作编辑

使用 Yjs 实现实时协作

Loading...

核心特性

  • 多提供者支持:通过 Yjsslate-yjs 实现实时协作。支持多个同步提供者(如 Hocuspocus + WebRTC)同时操作共享的 Y.Doc
  • 内置提供者:开箱即用支持 Hocuspocus(服务端方案)和 WebRTC(点对点方案)。
  • 自定义提供者:通过实现 UnifiedProvider 接口可扩展自定义提供者(如 IndexedDB 离线存储)。
  • 状态感知与光标:集成 Yjs Awareness 协议共享光标位置等临时状态,包含 RemoteCursorOverlay 组件渲染远程光标。
  • 可定制光标:通过 cursors 配置光标外观(名称、颜色)。
  • 手动生命周期:提供明确的 initdestroy 方法管理 Yjs 连接。

使用指南

安装

安装核心 Yjs 插件和所需提供者包:

pnpm add @platejs/yjs

Hocuspocus 服务端方案:

pnpm add @hocuspocus/provider

WebRTC 点对点方案:

pnpm add y-webrtc

添加插件

import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    YjsPlugin,
  ],
  // 重要:使用 Yjs 时需跳过 Plate 的默认初始化
  skipInitialization: true,
});

必要编辑器配置

创建编辑器时必须设置 skipInitialization: true。Yjs 负责管理初始文档状态,跳过 Plate 的默认值初始化可避免冲突。

配置 YjsPlugin

配置插件提供者和光标设置:

import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
import { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay';
 
const editor = createPlateEditor({
  plugins: [
    // ...其他插件
    YjsPlugin.configure({
      render: {
        afterEditable: RemoteCursorOverlay,
      },
      options: {
        // 配置本地用户光标外观
        cursors: {
          data: {
            name: '用户名', // 替换为动态用户名
            color: '#aabbcc', // 替换为动态用户颜色
          },
        },
        // 配置提供者(所有提供者共享同一个 Y.Doc 和 Awareness 实例)
        providers: [
          // Hocuspocus 提供者示例
          {
            type: 'hocuspocus',
            options: {
              name: '我的文档ID', // 文档唯一标识
              url: 'ws://localhost:8888', // Hocuspocus 服务地址
            },
          },
          // WebRTC 提供者示例(可与 Hocuspocus 同时使用)
          {
            type: 'webrtc',
            options: {
              roomName: '我的文档ID', // 需与文档标识一致
              signaling: ['ws://localhost:4444'], // 可选:信令服务器地址
            },
          },
        ],
      },
    }),
  ],
  skipInitialization: true,
});
  • render.afterEditable:指定 RemoteCursorOverlay 渲染远程用户光标。
  • cursors.data:配置本地用户光标显示名称和颜色。
  • providers:协作提供者数组(Hocuspocus、WebRTC 或自定义提供者)。

添加编辑器容器

RemoteCursorOverlay 需要定位容器包裹编辑器内容,使用 EditorContainerplatejs/reactPlateContainer

import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';
 
return (
  <Plate editor={editor}>
    <EditorContainer>
      <Editor />
    </EditorContainer>
  </Plate>
);

初始化 Yjs 连接

Yjs 连接和状态需手动初始化(通常在 useEffect 中处理):

import React, { useEffect } from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { useMounted } from '@/hooks/use-mounted'; // 或自定义挂载检查
 
const MyEditorComponent = ({ documentId, initialValue }) => {
  const editor = usePlateEditor(/** 前文配置 **/);
  const mounted = useMounted();
 
  useEffect(() => {
    if (!mounted) return;
 
    // 初始化 Yjs 连接并设置初始状态
    editor.getApi(YjsPlugin).yjs.init({
      id: documentId,          // Yjs 文档唯一标识
      value: initialValue,     // Y.Doc 为空时的初始内容
    });
 
    // 清理:组件卸载时销毁连接
    return () => {
      editor.getApi(YjsPlugin).yjs.destroy();
    };
  }, [editor, mounted]);
 
  return (
    <Plate editor={editor}>
      <EditorContainer>
        <Editor />
      </EditorContainer>
    </Plate>
  );
};

初始值initvalue 仅在后台/对等网络中文档完全空时生效。若文档已存在,将同步现有内容并忽略该值。

生命周期管理:必须调用 editor.api.yjs.init() 建立连接,并在组件卸载时调用 editor.api.yjs.destroy() 清理资源。

监控连接状态(可选)

访问提供者状态并添加事件监听:

import React from 'react';
import { YjsPlugin } from '@platejs/yjs/react';
import { usePluginOption } from 'platejs/react';
 
function EditorStatus() {
  // 直接访问提供者状态(只读)
  const providers = usePluginOption(YjsPlugin, '_providers');
  const isConnected = usePluginOption(YjsPlugin, '_isConnected');
 
  return (
    <div>
      {providers.map((provider) => (
        <span key={provider.type}>
          {provider.type}: {provider.isConnected ? '已连接' : '未连接'} ({provider.isSynced ? '已同步' : '同步中'})
        </span>
      ))}
    </div>
  );
}
 
// 添加连接事件处理器:
YjsPlugin.configure({
  options: {
    // ... 其他配置
    onConnect: ({ type }) => console.debug(`${type} 提供者已连接!`),
    onDisconnect: ({ type }) => console.debug(`${type} 提供者已断开`),
    onSyncChange: ({ type, isSynced }) => console.debug(`${type} 提供者同步状态: ${isSynced}`),
    onError: ({ type, error }) => console.error(`${type} 提供者错误:`, error),
  },
});

提供者类型

Hocuspocus 提供者

基于 Hocuspocus 的服务端方案,需运行 Hocuspocus 服务。

type HocuspocusProviderConfig = {
  type: 'hocuspocus',
  options: {
    name: string;     // 文档标识
    url: string;      // WebSocket 服务地址
    token?: string;   // 认证令牌
  }
}

WebRTC 提供者

基于 y-webrtc 的点对点方案。

type WebRTCProviderConfig = {
  type: 'webrtc',
  options: {
    roomName: string;      // 协作房间名
    signaling?: string[];  // 信令服务器地址
    password?: string;     // 房间密码
    maxConns?: number;    // 最大连接数
    peerOpts?: object;    // WebRTC 对等选项
  }
}

自定义提供者

通过实现 UnifiedProvider 接口创建自定义提供者:

interface UnifiedProvider {
  awareness: Awareness;
  document: Y.Doc;
  type: string;
  connect: () => void;
  destroy: () => void;
  disconnect: () => void;
  isConnected: boolean;
  isSynced: boolean;
}

直接在提供者数组中使用:

const customProvider = new MyCustomProvider({ doc: ydoc, awareness });
 
YjsPlugin.configure({
  options: {
    providers: [customProvider],
  },
});

后端配置

Hocuspocus 服务

搭建 Hocuspocus 服务,确保提供者配置中的 urlname 与服务端匹配。

WebRTC 配置

信令服务器

WebRTC 需信令服务器进行节点发现。测试可使用公共服务器,生产环境建议自建:

pnpm add y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.js

客户端配置自定义信令:

{
  type: 'webrtc',
  options: {
    roomName: '文档-1',
    signaling: ['ws://您的信令服务器:4444'],
  },
}

TURN 服务器

WebRTC 连接可能因防火墙失败。生产环境建议使用 TURN 服务器或结合 Hocuspocus。

配置 TURN 服务器提升连接可靠性:

{
  type: 'webrtc',
  options: {
    roomName: '文档-1',
    signaling: ['ws://您的信令服务器:4444'],
    peerOpts: {
      config: {
        iceServers: [
          { urls: 'stun:stun.l.google.com:19302' },
          {
            urls: 'turn:您的TURN服务器:3478',
            username: '用户名',
            credential: '密码'
          }
        ]
      }
    }
  }
}

安全实践

认证与授权:

  • 使用 Hocuspocus 的 onAuthenticate 钩子验证用户
  • 后端实现文档级访问控制
  • 通过 token 选项传递认证令牌

传输安全:

  • 生产环境使用 wss:// 加密通信
  • 配置 turns:// 协议的 TURN 服务器

WebRTC 安全:

  • 使用 password 选项控制房间访问
  • 配置安全信令服务器

安全配置示例:

YjsPlugin.configure({
  options: {
    providers: [
      {
        type: 'hocuspocus',
        options: {
          name: '安全文档ID',
          url: 'wss://您的Hocuspocus服务',
          token: '用户认证令牌',
        },
      },
      {
        type: 'webrtc',
        options: {
          roomName: '安全文档ID',
          password: '高强度房间密码',
          signaling: ['wss://您的安全信令服务'],
          peerOpts: {
            config: {
              iceServers: [
                {
                  urls: 'turns:您的TURN服务器:443?transport=tcp',
                  username: '用户',
                  credential: '密码'
                }
              ]
            }
          }
        },
      },
    ],
  },
});

问题排查

连接问题

检查地址与名称:

  • 确认 Hocuspocus 的 url 和 WebRTC 的 signaling 地址正确
  • 确保所有协作者的 nameroomName 完全一致
  • 开发环境使用 ws://,生产环境使用 wss://

服务状态:

  • 确认 Hocuspocus 和信令服务正常运行
  • 检查服务端日志错误
  • WebRTC 需测试 TURN 服务器连通性

网络问题:

  • 防火墙可能阻止 WebSocket/WebRTC 流量
  • 配置 TCP 443 端口的 TURN 服务器提升穿透能力
  • 浏览器控制台查看提供者错误

多文档处理

独立实例:

  • 每个文档创建独立的 Y.Doc 实例
  • 使用唯一的文档标识作为 name/roomName
  • 为每个编辑器传递独立的 ydocawareness 实例

同步问题

编辑器初始化:

  • 创建编辑器时始终设置 skipInitialization: true
  • 使用 editor.api.yjs.init({ value }) 设置初始内容
  • 确保所有提供者使用完全相同的文档标识

内容冲突:

  • 避免手动操作共享的 Y.Doc
  • 所有文档操作通过编辑器由 Yjs 处理

光标问题

悬浮层配置:

  • 插件配置中包含 RemoteCursorOverlay
  • 使用定位容器(EditorContainerPlateContainer
  • 确认本地用户的 cursors.data(名称、颜色)配置正确

相关资源

插件

YjsPlugin

通过 Yjs 实现实时协作,支持多提供者和远程光标。

Options

  • providers (UnifiedProvider | YjsProviderConfig)[]

    提供者配置数组或已实例化的提供者。插件会根据配置创建实例或直接使用现有实例。所有提供者共享同一个 Y.Doc 和 Awareness。每个配置对象需指定提供者 type(如 'hocuspocus''webrtc')及其专属 options。自定义提供者实例需符合 UnifiedProvider 接口。

  • cursors optional WithCursorsOptions | null

    远程光标配置。设为 null 显式禁用光标。未指定时,若配置了提供者则默认启用。参数传递给 withTCursors,详见 WithCursorsOptions API。包含本地用户信息的 data 和默认 trueautoSend

  • ydoc optional Y.Doc

    可选共享 Y.Doc 实例。未提供时插件会内部创建。需与其他 Yjs 工具集成或管理多文档时建议自行提供。

  • awareness optional Awareness

    可选共享 Awareness 实例。未提供时插件会内部创建。

  • onConnect optional (props: { type: YjsProviderType }) => void

    任一提供者成功连接时的回调。

  • onDisconnect optional (props: { type: YjsProviderType }) => void

    任一提供者断开连接时的回调。

  • onError optional (props: { error: Error; type: YjsProviderType }) => void

    任一提供者发生错误(如连接失败)时的回调。

  • onSyncChange optional (props: { isSynced: boolean; type: YjsProviderType }) => void

    任一提供者同步状态 (provider.isSynced) 变化时的回调。

Attributes

Collapse all
  • _isConnected boolean

    内部状态:至少一个提供者已连接时为 true。

  • _isSynced boolean

    内部状态:反映整体同步状态。

  • _providers UnifiedProvider[]

    内部状态:所有活跃提供者实例数组。

API

api.yjs.init

初始化 Yjs 连接,将其绑定到编辑器,根据插件配置设置提供者,可能填充 Y.Doc 的初始内容,并连接提供者。必须在编辑器挂载后调用。

Parameters

Collapse all
  • options optional object

    初始化配置对象。

Optionsobject

Collapse all
  • id optional string

    Yjs 文档的唯一标识符(如房间名、文档 ID)。未提供时使用 editor.id。确保协作者连接到同一文档状态的关键。

  • value optional Value | string | ((editor: PlateEditor) => Value | Promise<Value>)

    编辑器的初始内容。**仅当共享状态(后端/对等端)中与 id 关联的 Y.Doc 完全为空时应用。**如果文档已存在,将同步其内容并忽略此值。可以是 Plate JSON(Value)、HTML 字符串或返回/解析为 Value 的函数。如果省略或为空,且 Y.Doc 为新文档,则使用默认空段落初始化。

  • autoConnect optional boolean

    是否在初始化期间自动调用所有配置提供者的 provider.connect()。默认:true。如果要使用 editor.api.yjs.connect() 手动管理连接,请设置为 false

  • autoSelect optional 'start' | 'end'

    如果设置,在初始化和同步后自动聚焦编辑器并将光标放置在文档的 'start' 或 'end' 位置。

  • selection optional Location

    初始化后设置选择的具体 Plate Location,覆盖 autoSelect

ReturnsPromise<void>

    初始设置(包括潜在的异步 value 解析和 YjsEditor 绑定)完成时解析。注意提供者连接和同步是异步进行的。

api.yjs.destroy

断开所有提供者连接,清理 Yjs 绑定(将编辑器从 Y.Doc 分离),并销毁 awareness 实例。必须在编辑器组件卸载时调用以防止内存泄漏和过时连接。

api.yjs.connect

手动连接到提供者。在 init 期间使用 autoConnect: false 时很有用。

Parameters

Collapse all
  • type optional YjsProviderType | YjsProviderType[]

    如果提供,仅连接到指定类型的提供者。如果省略,连接到所有尚未连接的已配置提供者。

api.yjs.disconnect

手动断开与提供者的连接。

Parameters

Collapse all
  • type optional YjsProviderType | YjsProviderType[]

    如果提供,仅断开与指定类型提供者的连接。如果省略,断开与所有当前已连接提供者的连接。