ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â ð shadcn/directory/udecode/plate/(plugins)/(collaboration)/yjs.cn â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â
Y.DocãUnifiedProvider æ¥å£å¯æ©å±èªå®ä¹æäŸè
ïŒåŠ IndexedDB 犻线ååšïŒãRemoteCursorOverlay ç»ä»¶æž²æè¿çšå
æ ãcursors é
眮å
æ å€è§ïŒåç§°ãé¢è²ïŒãinit å destroy æ¹æ³ç®¡ç Yjs è¿æ¥ãå®è£ æ žå¿ Yjs æä»¶åæéæäŸè å ïŒ
npm install @platejs/yjs
Hocuspocus æå¡ç«¯æ¹æ¡ïŒ
npm install @hocuspocus/provider
WebRTC ç¹å¯¹ç¹æ¹æ¡ïŒ
npm install y-webrtc
import { YjsPlugin } from '@platejs/yjs/react';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...å
¶ä»æä»¶
YjsPlugin,
],
// éèŠïŒäœ¿çš Yjs æ¶éè·³è¿ Plate çé»è®€åå§å
skipInitialization: true,
});
<Callout type="warning" title="å¿
èŠçŒèŸåšé
眮">
å建çŒèŸåšæ¶å¿
须讟眮 `skipInitialization: true`ãYjs èŽèŽ£ç®¡çåå§ææ¡£ç¶æïŒè·³è¿ Plate çé»è®€åŒåå§åå¯é¿å
å²çªã
</Callout>
é 眮æä»¶æäŸè åå æ 讟眮ïŒ
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 éèŠå®äœå®¹åšå
裹çŒèŸåšå
容ïŒäœ¿çš EditorContainer æ platejs/react ç PlateContainerïŒ
import { Plate } from 'platejs/react';
import { EditorContainer } from '@/components/ui/editor';
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);
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>
);
};
<Callout>
**åå§åŒ**ïŒ`init` ç `value` ä»
åšåå°/对ççœç»äžææ¡£å®å
šç©ºæ¶çæãè¥ææ¡£å·²ååšïŒå°åæ¥ç°æå
容并応ç¥è¯¥åŒã
çåœåšæç®¡çïŒå¿
é¡»è°çš editor.api.yjs.init() 建ç«è¿æ¥ïŒå¹¶åšç»ä»¶åžèœœæ¶è°çš editor.api.yjs.destroy() æž
çèµæºã
</Callout>
è®¿é®æäŸè ç¶æå¹¶æ·»å äºä»¶çå¬ïŒ
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 æå¡ã
type HocuspocusProviderConfig = {
type: 'hocuspocus',
options: {
name: string; // ææ¡£æ è¯
url: string; // WebSocket æå¡å°å
token?: string; // 讀è¯ä»€ç
}
}
åºäº 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 æå¡ïŒç¡®ä¿æäŸè
é
眮äžç url å name äžæå¡ç«¯å¹é
ã
WebRTC é信什æå¡åšè¿è¡èç¹åç°ãæµè¯å¯äœ¿çšå ¬å ±æå¡åšïŒç产ç¯å¢å»ºè®®èªå»ºïŒ
npm install y-webrtc
PORT=4444 node ./node_modules/y-webrtc/bin/server.js
客æ·ç«¯é 眮èªå®ä¹ä¿¡ä»€ïŒ
{
type: 'webrtc',
options: {
roomName: 'ææ¡£-1',
signaling: ['ws://æšç信什æå¡åš:4444'],
},
}
é 眮 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: 'å¯ç '
}
]
}
}
}
}
讀è¯äžææïŒ
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: 'å¯ç '
}
]
}
}
},
},
],
},
});
æ£æ¥å°åäžåç§°ïŒ
url å WebRTC ç signaling å°åæ£ç¡®name æ roomName å®å
šäžèŽws://ïŒç产ç¯å¢äœ¿çš wss://æå¡ç¶æïŒ
çœç»é®é¢ïŒ
ç¬ç«å®äŸïŒ
Y.Doc å®äŸname/roomNameydoc å awareness å®äŸçŒèŸåšåå§åïŒ
skipInitialization: trueeditor.api.yjs.init({ value }) 讟眮åå§å
容å 容å²çªïŒ
Y.Docæ¬æµ®å±é 眮ïŒ
RemoteCursorOverlayEditorContainer æ PlateContainerïŒ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` åé»è®€ `true` ç `autoSend`ã </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> {/* å éšç¶æïŒéåžžäœ¿çš options æäºä»¶å€çåšæ¿ä»£ */} <APIItem name="_isConnected" type="boolean"> å éšç¶æïŒè³å°äžäžªæäŸè å·²è¿æ¥æ¶äžº trueã </APIItem> <APIItem name="_isSynced" type="boolean"> å éšç¶æïŒåæ æŽäœåæ¥ç¶æã </APIItem> <APIItem name="_providers" type="UnifiedProvider[]"> å éšç¶æïŒæææŽ»è·æäŸè å®äŸæ°ç»ã </APIItem> </APIAttributes> </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> åŠæè®Ÿçœ®ïŒåšåå§åå忥åèªåšèçŠçŒèŸåšå¹¶å°å æ æŸçœ®åšææ¡£ç 'start' æ 'end' äœçœ®ã </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.yjs.disconnectæåšæåŒäžæäŸè çè¿æ¥ã
<API name="editor.api.yjs.disconnect"> <APIParameters> <APIItem name="type" type="YjsProviderType | YjsProviderType[]" optional> åŠææäŸïŒä» æåŒäžæå®ç±»åæäŸè çè¿æ¥ãåŠæçç¥ïŒæåŒäžææåœåå·²è¿æ¥æäŸè çè¿æ¥ã </APIItem> </APIParameters> </API>â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ