Back to Plate

协作

docs/(plugins)/(collaboration)/yjs.cn.mdx

1.0.018.2 KB
Original Source
<ComponentPreview name="collaboration-demo" /> <PackageInfo>

功能特性

  • 多提供者支持: 使用 Yjsslate-yjs 实现实时协作。支持同时使用多个同步提供者(例如 Hocuspocus + WebRTC)共享同一个 Y.Doc
  • 内置提供者: 开箱即用地支持 Hocuspocus(服务器端)和 WebRTC(点对点)提供者。
  • 自定义提供者: 可扩展架构允许通过实现 UnifiedProvider 接口添加自定义提供者(例如用于离线存储的 IndexedDB)。
  • 感知与光标: 集成 Yjs Awareness 协议,用于在用户之间共享光标位置和其他临时状态。包含 RemoteCursorOverlay 用于渲染远程光标。
  • 可自定义光标: 可通过 cursors 自定义光标外观(名称、颜色)。
  • 手动生命周期: 提供显式的 initdestroy 方法来管理 Yjs 连接生命周期。
</PackageInfo>

使用方法

<Steps>

安装

安装核心 Yjs 插件和您打算使用的特定提供者包:

bash
npm install @platejs/yjs

用于 Hocuspocus 服务器端协作:

bash
npm install @hocuspocus/provider

用于 WebRTC 点对点协作:

bash
npm install y-webrtc

添加插件

tsx
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';

const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    YjsPlugin,
  ],
  // 重要:使用 Yjs 时跳过 Plate 的默认初始化
  skipInitialization: true,
});
<Callout type="warning" title="必需的编辑器配置"> 创建编辑器时必须设置 `skipInitialization: true`。Yjs 管理初始文档状态,因此应跳过 Plate 的默认值初始化以避免冲突。 </Callout>

配置 YjsPlugin

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

tsx
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
import { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay';

const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    YjsPlugin.configure({
      render: {
        afterEditable: RemoteCursorOverlay,
      },
      options: {
        // 配置本地用户光标外观
        cursors: {
          data: {
            name: 'User Name', // 替换为动态用户名
            color: '#aabbcc', // 替换为动态用户颜色
          },
        },
        // 配置提供者。所有提供者共享同一个 Y.Doc 和 Awareness 实例。
        providers: [
          // 示例:Hocuspocus 提供者
          {
            type: 'hocuspocus',
            options: {
              name: 'my-document-id', // 文档的唯一标识符
              url: 'ws://localhost:8888', // 您的 Hocuspocus 服务器 URL
            },
          },
          // 示例:WebRTC 提供者(可与 Hocuspocus 一起使用)
          {
            type: 'webrtc',
            options: {
              roomName: 'my-document-id', // 必须与文档标识符匹配
              signaling: ['ws://localhost:4444'], // 可选:您的信令服务器 URL
            },
          },
        ],
      },
    }),
  ],
  skipInitialization: true,
});
  • render.afterEditable: 指定 RemoteCursorOverlay 来渲染远程用户光标。
  • cursors.data: 配置本地用户的光标外观,包括名称和颜色。
  • providers: 要使用的协作提供者数组(Hocuspocus、WebRTC 或自定义提供者)。

添加编辑器容器

RemoteCursorOverlay 需要在编辑器内容周围有一个定位容器。使用 EditorContainer 组件或 platejs/react 中的 PlateContainer

tsx
import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';

return (
  <Plate editor={editor}>
    <EditorContainer>
      <Editor />
    </EditorContainer>
  </Plate>
);

初始化 Yjs 连接

Yjs 连接和状态初始化需要手动处理,通常在 useEffect 钩子中完成:

tsx
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>
  );
};
<Callout> **初始值**: 传递给 `init` 的 `value` 仅在后端/对等网络上的 Y.Doc 完全为空时用于填充文档。如果文档已存在,其内容将被同步,此初始值将被忽略。

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

监控连接状态(可选)

访问提供者状态并添加事件处理器来监控连接:

tsx
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),
  },
});
</Steps>

提供者类型

Hocuspocus 提供者

使用 Hocuspocus 的服务器端协作。需要运行中的 Hocuspocus 服务器。

tsx
type HocuspocusProviderConfig = {
  type: 'hocuspocus',
  options: {
    name: string;     // 文档标识符
    url: string;      // WebSocket 服务器 URL
    token?: string;   // 认证令牌
    wsOptions?: HocuspocusProviderWebsocketConfiguration; // 高级 websocket 配置(headers、协议等)
  }
}

wsOptions

您可以传递 wsOptions 字段来配置 Hocuspocus 提供者的高级 websocket 选项。这对于自定义 headers、认证、协议或 HocuspocusProviderWebsocket 支持的其他 websocket 设置非常有用。

示例用法:

tsx
{
  type: 'hocuspocus',
  options: {
    name: 'my-document-id',
  },
  wsOptions: {
    url: 'ws://localhost:8888',
    maxAttempts: 5,
    parameters: {
      // 请求参数
    }
  },
}

WebRTC 提供者

使用 y-webrtc 的点对点协作。

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

自定义提供者

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

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

在 providers 数组中直接使用自定义提供者:

tsx
const customProvider = new MyCustomProvider({ doc: ydoc, awareness });

YjsPlugin.configure({
  options: {
    providers: [customProvider],
  },
});

后端设置

Hocuspocus 服务器

为服务器端协作设置 Hocuspocus 服务器。确保提供者选项中的 urlname 与服务器配置匹配。

WebRTC 设置

信令服务器

WebRTC 需要信令服务器进行对等发现。公共服务器可用于测试,但生产环境应使用自己的服务器:

bash
npm install y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.js

配置客户端使用自定义信令:

tsx
{
  type: 'webrtc',
  options: {
    roomName: 'document-1',
    signaling: ['ws://your-signaling-server.com:4444'],
  },
}

TURN 服务器

<Callout type="warning"> 由于防火墙原因,WebRTC 连接可能会失败。生产环境中使用 TURN 服务器或与 Hocuspocus 结合使用以确保可靠性。 </Callout>

配置 TURN 服务器以获得可靠连接:

tsx
{
  type: 'webrtc',
  options: {
    roomName: 'document-1',
    signaling: ['ws://your-signaling-server.com:4444'],
    peerOpts: {
      config: {
        iceServers: [
          { urls: 'stun:stun.l.google.com:19302' },
          {
            urls: 'turn:your-turn-server.com:3478',
            username: 'username',
            credential: 'password'
          }
        ]
      }
    }
  }
}

安全性

认证与授权:

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

传输安全:

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

WebRTC 安全:

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

安全配置示例:

tsx
YjsPlugin.configure({
  options: {
    providers: [
      {
        type: 'hocuspocus',
        options: {
          name: 'secure-document-id',
          url: 'wss://your-hocuspocus-server.com',
          token: 'user-auth-token',
        },
      },
      {
        type: 'webrtc',
        options: {
          roomName: 'secure-document-id',
          password: 'strong-room-password',
          signaling: ['wss://your-secure-signaling.com'],
          peerOpts: {
            config: {
              iceServers: [
                {
                  urls: 'turns:your-turn-server.com:443?transport=tcp',
                  username: 'user',
                  credential: 'pass'
                }
              ]
            }
          }
        },
      },
    ],
  },
});

故障排除

连接问题

检查 URL 和名称:

  • 验证 url(Hocuspocus)和 signaling URL(WebRTC)是否正确
  • 确保 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 启用实时协作,支持多个提供者和远程光标。

<API name="YjsPlugin"> <APIOptions> <APIItem name="providers" type="(UnifiedProvider | YjsProviderConfig)[]"> 提供者配置数组或预实例化的提供者实例。插件将从配置创建实例并直接使用现有实例。所有提供者将共享同一个 Y.Doc 和 Awareness。每个配置对象指定提供者 `type`(例如 `'hocuspocus'`、`'webrtc'`)及其特定的 `options`。自定义提供者实例必须符合 `UnifiedProvider` 接口。 </APIItem> <APIItem name="cursors" type="WithCursorsOptions | null" optional> 远程光标配置。设置为 `null` 可显式禁用光标。如果省略,当指定了提供者时默认启用光标。传递给 `withTCursors`。参见 [WithCursorsOptions API](https://docs.slate-yjs.dev/api/slate-yjs-core/cursor-plugin#withcursors)。包括 `data`(本地用户信息)和 `autoSend`(默认 `true`)。 </APIItem> <APIItem name="ydoc" type="Y.Doc" optional> 可选的共享 Y.Doc 实例。如果未提供,插件将在内部创建一个新实例。如果与其他 Yjs 工具集成或管理多个文档,可以提供您自己的实例。 </APIItem> <APIItem name="awareness" type="Awareness" optional> 可选的共享 Awareness 实例。如果未提供,将创建一个新实例。 </APIItem> <APIItem name="onConnect" type="(props: { type: YjsProviderType }) => void" optional> 任何提供者成功连接时触发的回调。 </APIItem> <APIItem name="onDisconnect" type="(props: { type: YjsProviderType }) => void" optional> 任何提供者断开连接时触发的回调。 </APIItem> <APIItem name="onError" type="(props: { error: Error; type: YjsProviderType }) => void" optional> 任何提供者遇到错误时触发的回调(例如连接失败)。 </APIItem> <APIItem name="onSyncChange" type="(props: { isSynced: boolean; type: YjsProviderType }) => void" optional> 任何单个提供者的同步状态(`provider.isSynced`)发生变化时触发的回调。 </APIItem> </APIOptions> <APIAttributes> <APIItem name="_isConnected" type="boolean"> 内部状态:是否至少有一个提供者当前已连接。 </APIItem> <APIItem name="_isSynced" type="boolean"> 内部状态:反映整体同步状态。 </APIItem> <APIItem name="_providers" type="UnifiedProvider[]"> 内部状态:所有活动的、已实例化的提供者实例数组。 </APIItem> </APIAttributes> </API>

API

api.yjs.init

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

<API name="editor.api.yjs.init"> <APIParameters> <APIItem name="options" type="object" optional> 初始化的配置对象。 </APIItem> </APIParameters> <APIOptions type="object"> <APIItem name="id" type="string" optional> Yjs 文档的唯一标识符(例如房间名称、文档 ID)。如果未提供,将使用 `editor.id`。对于确保协作者连接到相同的文档状态至关重要。 </APIItem> <APIItem name="value" type="Value | string | ((editor: PlateEditor) => Value | Promise<Value>)" optional> 编辑器的初始内容。**仅当共享状态(后端/对等端)中与 `id` 关联的 Y.Doc 完全为空时才应用此内容。**如果文档已存在,其内容将被同步,忽略此值。可以是 Plate JSON(`Value`)、HTML 字符串或返回/解析为 `Value` 的函数。如果省略或为空,当 Y.Doc 是新的时将使用默认的空段落进行初始化。 </APIItem> <APIItem name="autoConnect" type="boolean" optional> 是否在初始化期间自动为所有配置的提供者调用 `provider.connect()`。默认:`true`。如果您想使用 `editor.api.yjs.connect()` 手动管理连接,请设置为 `false`。 </APIItem> <APIItem name="autoSelect" type="'start' | 'end'" optional> 如果设置,初始化和同步后自动聚焦编辑器并将光标放置在文档的"开头"或"结尾"。 </APIItem> <APIItem name="selection" type="Location" optional> 初始化后要设置的特定 Plate `Location`,会覆盖 `autoSelect`。 </APIItem> </APIOptions> <APIReturns type="Promise<void>"> 初始设置(包括可能的异步 `value` 解析和 YjsEditor 绑定)完成时解析。注意提供者连接和同步是异步进行的。 </APIReturns> </API>

api.yjs.destroy

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

api.yjs.connect

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

<API name="editor.api.yjs.connect"> <APIParameters> <APIItem name="type" type="YjsProviderType | YjsProviderType[]" optional> 如果提供,仅连接到指定类型的提供者。如果省略,连接到所有尚未连接的已配置提供者。 </APIItem> </APIParameters> </API>

api.yjs.disconnect

手动断开提供者连接。

<API name="editor.api.yjs.disconnect"> <APIParameters> <APIItem name="type" type="YjsProviderType | YjsProviderType[]" optional> 如果提供,仅断开指定类型的提供者连接。如果省略,断开所有当前已连接的提供者。 </APIItem> </APIParameters> </API>