基于Yjs+CodeMirror+ActionCable的协同编辑器无报错但无法跨端同步
Rails + y-rb-actioncable + Angular Yjs 协同编辑器跨端同步问题排查
我用Rails后端的yrb-actioncable搭配Angular前端的Yjs构建协同文本编辑器,刚接触这些技术,解决完报错后无错误提示,但仅同一浏览器标签页能协同编辑,ActionCable能看到请求交互,但跨端(不同浏览器/设备)无法同步内容。
后端DocumentChannel代码
require 'y-rb' module ApplicationCable class DocumentChannel < ApplicationCable::Channel include Y::Actioncable::Sync def initialize(connection, identifier, params = nil) super load { |id| load_doc 1 } end def subscribed sync_from("document-1") end def receive(data) sync_to("document-1", data) end def unsubscribed end private def load_doc(id) doc_content = Document.first.content ydoc = Y::Doc.new ytext = ydoc.get_text('mine') ytext << doc_content data = [] data = ydoc.diff unless doc_content.nil? data end end end
前端CodeEditorComponent代码
import { Component } from '@angular/core'; import { Observable } from 'rxjs'; import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http' import { OnInit } from '@angular/core'; import * as Y from "yjs"; import { WebsocketProvider } from "@y-rb/actioncable"; import CodeMirror from "codemirror"; import { CodeMirrorBinding } from 'y-codemirror' import ActionCable from 'actioncable' @Component({ selector: 'code-editor', templateUrl: './code-editor.component.html', styleUrls: ['./code-editor.component.scss'] }) export class CodeEditorComponent implements OnInit { constructor(private http: HttpClient) { } ngOnInit() { const accessToken = localStorage.getItem('accessToken') const uid = localStorage.getItem('uid') const client = localStorage.getItem('client') const yDocument = new Y.Doc(); const consumer = ActionCable.createConsumer(`ws://localhost:3000/cable?uid=${uid}&access-token=${accessToken}&client=${client}`); const provider = new WebsocketProvider( yDocument, consumer, "ApplicationCable::DocumentChannel", {} ); const yText = yDocument.getText('codemirror') const yUndoManager = new Y.UndoManager(yText) const editorContainer = document.createElement('div') editorContainer.setAttribute('id', 'editor') document.body.insertBefore(editorContainer, null) const editor = CodeMirror(editorContainer, { mode: 'javascript', lineNumbers: true, }) const binding = new CodeMirrorBinding(yText, editor, provider.awareness, { yUndoManager }) // @ts-ignore //window.example = { provider, ydoc, yText, binding, Y } } }
核心排查与修复方案
1. Yjs文本节点名称不匹配
后端load_doc中使用ydoc.get_text('mine'),但前端用的是yDocument.getText('codemirror')——两端操作的是不同的Yjs文本对象,自然无法同步。
修改后端load_doc方法:
def load_doc(id) doc_content = Document.first.content ydoc = Y::Doc.new # 改为和前端一致的节点名称 ytext = ydoc.get_text('codemirror') ytext << doc_content data = [] data = ydoc.diff unless doc_content.nil? data end
2. 文档标识与频道订阅参数不匹配
后端sync_from("document-1")指定了文档标识,但前端WebsocketProvider未传递对应参数,导致多客户端可能未订阅到同一文档频道。
修改前端WebsocketProvider初始化:
const provider = new WebsocketProvider( yDocument, consumer, "ApplicationCable::DocumentChannel", { documentId: "document-1" } // 明确传递文档标识,与后端对应 );
同时后端初始化时避免硬编码文档ID:
def initialize(connection, identifier, params = nil) super # 从回调参数接收文档ID,而非固定传1 load { |id| load_doc(id) } end
3. Yjs状态持久化逻辑缺失
后端每次创建新的Y::Doc,未从持久化状态加载,新客户端无法获取历史同步数据。建议在Document模型新增yjs_state二进制字段保存完整Yjs状态:
def load_doc(id) document = Document.find(id) if document.yjs_state.present? # 从数据库加载已保存的Yjs状态 ydoc = Y::Doc.new ydoc.sync(document.yjs_state) ydoc.diff else ydoc = Y::Doc.new ytext = ydoc.get_text('codemirror') ytext << document.content || "" # 保存初始状态到数据库 document.update(yjs_state: ydoc.encode) ydoc.diff end end
同步后更新数据库状态:
def subscribed sync_from("document-1") do |ydoc| # 每次同步完成后保存最新状态 document = Document.find(1) document.update(yjs_state: ydoc.encode) end end
4. 确认ActionCable连接独立性
检查后端connection.rb的用户认证逻辑,确保不同客户端生成独立连接实例:
# app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user logger.info "User connected: #{current_user.id}" end private def find_verified_user User.find_by(uid: request.params[:uid], access_token: request.params['access-token'], client: request.params[:client]) || reject_unauthorized_connection end end end
5. 前端编辑器DOM初始化优化
避免动态创建编辑器容器,直接在组件模板中定义:
<!-- code-editor.component.html --> <div id="editor"></div>
前端代码中直接获取容器:
const editorContainer = document.getElementById('editor'); if (!editorContainer) return; const editor = CodeMirror(editorContainer, { mode: 'javascript', lineNumbers: true, })
内容的提问来源于stack exchange,提问作者DJA
相关产品推荐
相关产品推荐

