使用Yjs实现在线协作编辑器:无需重连WebSocket切换文档
复用WebSocket连接实现文档切换
完全可以实现复用WebSocket连接加载不同文档,核心思路是将WebSocket连接(WebsocketProvider)和Y.Doc实例持久化,切换文档时仅更换关联的Y.Text对象,而非每次新建连接和文档实例。以下是具体实现方案:
关键原理
Yjs的WebsocketProvider与特定房间(即你的projectId)绑定,同一房间内可通过不同键名(对应你的docId)存储多个独立文档内容。因此只需在同一个Y.Doc下,为每个docId创建独立的Y.Text节点,即可复用现有WebSocket连接。
修改后的代码实现
import { useRef, useEffect } from "react"; import { EditorView } from "@codemirror/view"; import { basicSetup } from "codemirror"; import styles from "./CollarCodeEditor.module.css"; import { syntaxHighlighting, defaultHighlightStyle, StreamLanguage, } from '@codemirror/language'; import { stex } from '@codemirror/legacy-modes/mode/stex'; import * as Y from 'yjs'; import { yCollab } from 'y-codemirror.next'; import { EditorState } from "@codemirror/state"; import { javascript } from '@codemirror/lang-javascript'; import * as random from 'lib0/random'; import { WebsocketProvider } from 'y-websocket'; export const usercolors = [ { color: '#30bced', light: '#30bced33' }, { color: '#6eeb83', light: '#6eeb8333' }, { color: '#ffbc42', light: '#ffbc4233' }, { color: '#ecd444', light: '#ecd44433' }, { color: '#ee6352', light: '#ee635233' }, { color: '#9ac2c9', light: '#9ac2c933' }, { color: '#8acb88', light: '#8acb8833' }, { color: '#1be7ff', light: '#1be7ff33' } ]; const userColor = usercolors[random.uint32() % usercolors.length]; const extensions = [ EditorView.contentAttributes.of({ spellcheck: 'true' }), EditorView.lineWrapping, EditorView.theme({ '.cm-content': {}, '.cm-scroller': { maxHeight: '100vh', minHeight: '100vh', }, }), StreamLanguage.define(stex), syntaxHighlighting(defaultHighlightStyle), ]; export type EditorProps = { projectId: string; docId: string; }; const CollarCodeEditor: React.FC<EditorProps> = ({ projectId, docId }) => { const edContainer = useRef<any>(null); // 持久化核心实例,避免重复创建 const ydocRef = useRef<Y.Doc | null>(null); const wsProviderRef = useRef<WebsocketProvider | null>(null); const editorViewRef = useRef<EditorView | null>(null); const undoManagerRef = useRef<Y.UndoManager | null>(null); useEffect(() => { // 切换房间(projectId)时,重建连接和文档 if (!ydocRef.current || wsProviderRef.current?.room !== projectId) { // 清理旧资源 editorViewRef.current?.destroy(); undoManagerRef.current?.destroy(); wsProviderRef.current?.destroy(); ydocRef.current?.destroy(); // 创建新的文档与连接 const ydoc = new Y.Doc({ guid: projectId }); const wsProvider = new WebsocketProvider('wss://ws.example.com', projectId, ydoc); // 仅初始化一次用户状态 wsProvider.awareness.setLocalStateField('user', { name: 'Anonymous ' + Math.floor(Math.random() * 100), color: userColor.color, colorLight: userColor.light }); ydocRef.current = ydoc; wsProviderRef.current = wsProvider; } const ydoc = ydocRef.current!; const wsProvider = wsProviderRef.current!; // 清理旧编辑器与撤销管理器 editorViewRef.current?.destroy(); undoManagerRef.current?.destroy(); // 绑定当前docId对应的Y.Text节点 const ytext = ydoc.getText(docId); const undoManager = new Y.UndoManager(ytext); undoManagerRef.current = undoManager; // 创建新编辑器视图 const state = EditorState.create({ doc: ytext.toString(), extensions: [ basicSetup, javascript(), yCollab(ytext, wsProvider.awareness, { undoManager }) ] }); const view = new EditorView({ state, parent: edContainer.current, extensions: [basicSetup, extensions] }); editorViewRef.current = view; // 组件卸载时清理所有资源 return () => { editorViewRef.current?.destroy(); undoManagerRef.current?.destroy(); wsProviderRef.current?.destroy(); ydocRef.current?.destroy(); }; }, [projectId, docId]); // 监听房间与文档ID变化 return <div ref={edContainer} className={styles.container}></div>; } export default CollarCodeEditor;
核心改动说明
- 持久化实例:用
useRef保存Y.Doc、WebsocketProvider等核心对象,避免重复创建连接与文档。 - 区分房间与文档:仅当
projectId(房间)变化时才重建WebSocket连接;docId切换时仅更换Y.Text的键名,复用现有连接。 - 资源清理:每次切换时销毁旧编辑器视图与撤销管理器,组件卸载时清理所有实例,避免内存泄漏。
- 状态复用:用户状态仅在连接初始化时设置一次,切换文档时保持不变。
内容的提问来源于stack exchange,提问作者Dolphin
相关产品推荐
相关产品推荐

