You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 20:00:04