功能特性
- 多提供者支持: 使用 Yjs 和 slate-yjs 实现实时协作。支持同时使用多个同步提供者(例如 Hocuspocus + WebRTC)共享同一个
Y.Doc。 - 内置提供者: 开箱即用地支持 Hocuspocus(服务器端)和 WebRTC(点对点)提供者。
- 自定义提供者: 可扩展架构允许通过实现
UnifiedProvider接口添加自定义提供者(例如用于离线存储的 IndexedDB)。 - 感知与光标: 集成 Yjs Awareness 协议,用于在用户之间共享光标位置和其他临时状态。包含
RemoteCursorOverlay用于渲染远程光标。 - 可自定义光标: 可通过
cursors自定义光标外观(名称、颜色)。 - 手动生命周期: 提供显式的
init和destroy方法来管理 Yjs 连接生命周期。
使用方法
安装
安装核心 Yjs 插件和您打算使用的特定提供者包:
pnpm add @platejs/yjspnpm add @platejs/yjs用于 Hocuspocus 服务器端协作:
pnpm add @hocuspocus/providerpnpm add @hocuspocus/provider用于 WebRTC 点对点协作:
pnpm add y-webrtcpnpm add y-webrtc添加插件
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
YjsPlugin,
],
// 重要:使用 Yjs 时跳过 Plate 的默认初始化
skipInitialization: true,
});import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
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: [
// ...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,
});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:
import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);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>
);
};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>
);
};初始值: 传递给 init 的 value 仅在后端/对等网络上的 Y.Doc 完全为空时用于填充文档。如果文档已存在,其内容将被同步,此初始值将被忽略。
生命周期管理: 您必须调用 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),
},
});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 服务器 URL
token?: string; // 认证令牌
wsOptions?: HocuspocusProviderWebsocketConfiguration; // 高级 websocket 配置(headers、协议等)
}
}type HocuspocusProviderConfig = {
type: 'hocuspocus',
options: {
name: string; // 文档标识符
url: string; // WebSocket 服务器 URL
token?: string; // 认证令牌
wsOptions?: HocuspocusProviderWebsocketConfiguration; // 高级 websocket 配置(headers、协议等)
}
}wsOptions
您可以传递 wsOptions 字段来配置 Hocuspocus 提供者的高级 websocket 选项。这对于自定义 headers、认证、协议或 HocuspocusProviderWebsocket 支持的其他 websocket 设置非常有用。
示例用法:
{
type: 'hocuspocus',
options: {
name: 'my-document-id',
},
wsOptions: {
url: 'ws://localhost:8888',
maxAttempts: 5,
parameters: {
// 请求参数
}
},
}{
type: 'hocuspocus',
options: {
name: 'my-document-id',
},
wsOptions: {
url: 'ws://localhost:8888',
maxAttempts: 5,
parameters: {
// 请求参数
}
},
}WebRTC 提供者
使用 y-webrtc 的点对点协作。
type WebRTCProviderConfig = {
type: 'webrtc',
options: {
roomName: string; // 协作房间名称
signaling?: string[]; // 信令服务器 URL
password?: string; // 房间密码
maxConns?: number; // 最大连接数
peerOpts?: object; // WebRTC 对等选项
}
}type WebRTCProviderConfig = {
type: 'webrtc',
options: {
roomName: string; // 协作房间名称
signaling?: string[]; // 信令服务器 URL
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;
}interface UnifiedProvider {
awareness: Awareness;
document: Y.Doc;
type: string;
connect: () => void;
destroy: () => void;
disconnect: () => void;
isConnected: boolean;
isSynced: boolean;
}在 providers 数组中直接使用自定义提供者:
const customProvider = new MyCustomProvider({ doc: ydoc, awareness });
YjsPlugin.configure({
options: {
providers: [customProvider],
},
});const customProvider = new MyCustomProvider({ doc: ydoc, awareness });
YjsPlugin.configure({
options: {
providers: [customProvider],
},
});后端设置
Hocuspocus 服务器
为服务器端协作设置 Hocuspocus 服务器。确保提供者选项中的 url 和 name 与服务器配置匹配。
WebRTC 设置
信令服务器
WebRTC 需要信令服务器进行对等发现。公共服务器可用于测试,但生产环境应使用自己的服务器:
pnpm add y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.jspnpm add y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.js配置客户端使用自定义信令:
{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
},
}{
type: 'webrtc',
options: {
roomName: 'document-1',
signaling: ['ws://your-signaling-server.com:4444'],
},
}TURN 服务器
由于防火墙原因,WebRTC 连接可能会失败。生产环境中使用 TURN 服务器或与 Hocuspocus 结合使用以确保可靠性。
配置 TURN 服务器以获得可靠连接:
{
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'
}
]
}
}
}
}{
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选项进行基本的房间访问控制 - 配置安全的信令服务器
安全配置示例:
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'
}
]
}
}
},
},
],
},
});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)和signalingURL(WebRTC)是否正确 - 确保
name或roomName在所有协作者之间完全匹配 - 本地开发使用
ws://,生产环境使用wss://
服务器状态:
- 验证 Hocuspocus 和信令服务器是否正在运行
- 检查服务器日志中的错误
- 如果使用 WebRTC,测试 TURN 服务器连接
网络问题:
- 防火墙可能阻止 WebSocket 或 WebRTC 流量
- 使用配置为 TCP(端口 443)的 TURN 服务器以获得更好的穿透性
- 检查浏览器控制台中的提供者错误
多文档
独立实例:
- 为每个文档创建独立的
Y.Doc实例 - 为
name/roomName使用唯一的文档标识符 - 为每个编辑器传递唯一的
ydoc和awareness实例
同步问题
编辑器初始化:
- 创建编辑器时始终设置
skipInitialization: true - 使用
editor.api.yjs.init({ value })设置初始内容 - 确保所有提供者使用完全相同的文档标识符
内容冲突:
- 避免手动操作共享的
Y.Doc - 让 Yjs 通过编辑器处理所有文档操作
光标问题
覆盖层设置:
- 在插件渲染配置中包含
RemoteCursorOverlay - 使用定位容器(
EditorContainer或PlateContainer) - 验证本地用户的
cursors.data(名称、颜色)设置正确
相关资源
- Yjs - 用于协作的 CRDT 框架
- slate-yjs - Slate 的 Yjs 绑定
- Hocuspocus - Yjs 后端服务器
- y-webrtc - WebRTC 提供者
- RemoteCursorOverlay - 远程光标组件
- EditorContainer - 编辑器容器组件
插件
YjsPlugin
使用 Yjs 启用实时协作,支持多个提供者和远程光标。
提供者配置数组或预实例化的提供者实例。插件将从配置创建实例并直接使用现有实例。所有提供者将共享同一个 Y.Doc 和 Awareness。每个配置对象指定提供者 type(例如 'hocuspocus'、'webrtc')及其特定的 options。自定义提供者实例必须符合 UnifiedProvider 接口。
远程光标配置。设置为 null 可显式禁用光标。如果省略,当指定了提供者时默认启用光标。传递给 withTCursors。参见 WithCursorsOptions API。包括 data(本地用户信息)和 autoSend(默认 true)。
可选的共享 Y.Doc 实例。如果未提供,插件将在内部创建一个新实例。如果与其他 Yjs 工具集成或管理多个文档,可以提供您自己的实例。
可选的共享 Awareness 实例。如果未提供,将创建一个新实例。
任何提供者成功连接时触发的回调。
任何提供者断开连接时触发的回调。
任何提供者遇到错误时触发的回调(例如连接失败)。
任何单个提供者的同步状态(provider.isSynced)发生变化时触发的回调。
API
api.yjs.init
初始化 Yjs 连接,将其绑定到编辑器,根据插件配置设置提供者,可能用初始内容填充 Y.Doc,并连接提供者。必须在编辑器挂载后调用。
Yjs 文档的唯一标识符(例如房间名称、文档 ID)。如果未提供,将使用 editor.id。对于确保协作者连接到相同的文档状态至关重要。
编辑器的初始内容。**仅当共享状态(后端/对等端)中与 id 关联的 Y.Doc 完全为空时才应用此内容。**如果文档已存在,其内容将被同步,忽略此值。可以是 Plate JSON(Value)、HTML 字符串或返回/解析为 Value 的函数。如果省略或为空,当 Y.Doc 是新的时将使用默认的空段落进行初始化。
是否在初始化期间自动为所有配置的提供者调用 provider.connect()。默认:true。如果您想使用 editor.api.yjs.connect() 手动管理连接,请设置为 false。
如果设置,初始化和同步后自动聚焦编辑器并将光标放置在文档的"开头"或"结尾"。
初始化后要设置的特定 Plate Location,会覆盖 autoSelect。
api.yjs.destroy
断开所有提供者的连接,清理 Yjs 绑定(从 Y.Doc 分离编辑器),并销毁 awareness 实例。必须在编辑器组件卸载时调用以防止内存泄漏和过时连接。
api.yjs.connect
手动连接到提供者。当 init 期间使用了 autoConnect: false 时很有用。
api.yjs.disconnect
手动断开提供者连接。