如何在服务端初始化 Lexical 协作文档的初始内容以避免并发创建导致损坏?

如何在服务端初始化 Lexical 协作文档的初始内容以避免并发创建导致损坏? 如何在服务端初始化 Lexical 协作文档的初始内容以避免并发创建导致损坏【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical基于lexical/react的CollaborationPlugin与lexical/yjs配合y-websocket构建协作编辑器时必须决定由谁来创建文档的初始内容。官方协作文档collaboration/react.md给出的结论是生产环境中应在服务端引导bootstrap编辑器的初始内容——如果把初始化留给客户端两个客户端同时连接时可能各自尝试初始化内容最终导致文档损坏document corruption。本文按官方文档走通这条路径服务端用lexical/headless生成带初始内容的Y.Doc客户端编辑器连接后直接同步该文档。为什么初始内容不能留给客户端react.md 在 Initial editor content 一节明确说明了风险与对策In a production environment, you should bootstrap the editors initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.客户端侧其实存在一条便捷路径给CollaborationPlugin传initialEditorState和shouldBootstrap两个 props 让编辑器在本地初始化状态。但文档对这两个 props 的注释是// Dev-testing only, do not use in real-world cases.即只用于本地调试不能作为生产方案。与之配合的一个关键配置LexicalComposer的initialConfig中必须把editorState设为null。文档注释解释了原因——这表示编辑器不应尝试设置任何默认状态哪怕空状态把状态交给协作插件设置const initialConfig { // NOTE: This is critical for collaboration plugin to set editor state to null. It // would indicate that the editor should not try to set any default state // (not even empty one), and let collaboration plugin do it instead editorState: null, namespace: Demo, nodes: [], onError: (error: Error) { throw error; }, theme: {}, };准备依赖与 Yjs WebSocket 服务按 react.md 的 Getting started安装客户端最小依赖集npm i -S lexical/react lexical/yjs lexical react react-dom y-websocket yjs其中y-websocket是目前唯一官方支持的 Yjs 连接 provider其他 provider 可能也能用但文档只背书这一个。服务端引导初始内容需要无 DOM 环境运行的 Lexical。安装lexical/headlesslexical-headless/README.mdnpm install --save lexical/headless该包允许在 Node.js 等不依赖 DOM 的环境中使用editor.update()、editor.registerNodeTransform()、editor.registerUpdateListener()来创建、更新和遍历编辑器状态——这正是服务端生成初始Y.Doc需要的能力。启动 Yjs WebSocket 服务让不同浏览器窗口互相发现并同步 Lexical 状态。YPERSISTENCE参数把 Yjs 文档落盘保存服务重启后客户端可以重新连接并继续编辑HOSTlocalhost PORT1234 YPERSISTENCE./yjs-wss-db npx y-websocket在服务端创建带初始内容的 Y.Doc服务端引导使用的核心工具是 faq.md InitializingEditorStatefrom Yjs Document 一节给出的withHeadlessCollaborationEditor。它创建一个 headless 编辑器、本地Y.Doc和一个 no-op provider不连接任何消息分发基础设施并建立编辑器与 Y.Doc 之间的双向同步——文档更新会写入 Y.DocY.Doc 的变更会同步回编辑器。完整实现可直接保存为createHeadlessCollaborativeEditor.tsimport type {Binding, Provider} from lexical/yjs; import type { Klass, LexicalEditor, LexicalNode, LexicalNodeReplacement, SerializedEditorState, SerializedLexicalNode, } from lexical; import {createHeadlessEditor} from lexical/headless; import { createBinding, syncLexicalUpdateToYjs, syncYjsChangesToLexical, } from lexical/yjs; import {type YEvent, applyUpdate, Doc, Transaction} from yjs; export default function headlessConvertYDocStateToLexicalJSON( nodes: ReadonlyArrayKlassLexicalNode | LexicalNodeReplacement, yDocState: Uint8Array, ): SerializedEditorStateSerializedLexicalNode { return withHeadlessCollaborationEditor(nodes, (editor, binding) { applyUpdate(binding.doc, yDocState, {isUpdateRemote: true}); editor.update(() {}, {discrete: true}); return editor.getEditorState().toJSON(); }); } /** * Creates headless collaboration editor with no-op provider (since it wont * connect to message distribution infra) and binding. It also sets up * bi-directional synchronization between yDoc and editor */ function withHeadlessCollaborationEditorT( nodes: ReadonlyArrayKlassLexicalNode | LexicalNodeReplacement, callback: (editor: LexicalEditor, binding: Binding, provider: Provider) T, ): T { const editor createHeadlessEditor({ nodes, }); const id main; const doc new Doc(); const docMap new Map([[id, doc]]); const provider createNoOpProvider(); const binding createBinding(editor, provider, id, doc, docMap); const unsubscribe registerCollaborationListeners(editor, provider, binding); const res callback(editor, binding, provider); unsubscribe(); return res; } function registerCollaborationListeners( editor: LexicalEditor, provider: Provider, binding: Binding, ): () void { const unsubscribeUpdateListener editor.registerUpdateListener( ({ dirtyElements, dirtyLeaves, editorState, normalizedNodes, prevEditorState, tags, }) { if (tags.has(skip-collab) false) { syncLexicalUpdateToYjs( binding, provider, prevEditorState, editorState, dirtyElements, dirtyLeaves, normalizedNodes, tags, ); } }, ); const observer (events: ArrayYEventany, transaction: Transaction) { if (transaction.origin ! binding) { syncYjsChangesToLexical(binding, provider, events, false); } }; binding.root.getSharedType().observeDeep(observer); return () { unsubscribeUpdateListener(); binding.root.getSharedType().unobserveDeep(observer); }; } function createNoOpProvider(): Provider { const emptyFunction () {}; return { awareness: { getLocalState: () null, getStates: () new Map(), off: emptyFunction, on: emptyFunction, setLocalState: emptyFunction, }, connect: emptyFunction, disconnect: emptyFunction, off: emptyFunction, on: emptyFunction, }; }有了这个工具按 react.md 的示例在服务端创建已引导bootstrapped的Y.Docimport type {CreateEditorArgs} from lexical; import {$getRoot, $createParagraphNode} from lexical; import {Doc} from yjs; import {withHeadlessCollaborationEditor} from ./withHeadlessCollaborationEditor; function createBootstrappedYDoc(nodes: CreateEditorArgs[nodes]): Doc { return withHeadlessCollaborationEditor(nodes, (editor) { const yDoc new Doc(); editor.update(() { $getRoot().append($createParagraphNode()); }, {discrete: true}); return yDoc; }); }$getRoot().append($createParagraphNode())是文档给出的最小初始内容示例实际引导时把文档需要的初始节点写入editor.update的回调即可。由于 no-op provider 不对外发送消息这段引导过程完全发生在服务端进程内不存在两个客户端并发写入初始内容的竞争。客户端接入已引导的文档客户端编辑器按 react.md 的 Getting started 配置核心是让CollaborationPlugin通过 provider 连接到与文档id对应的房间function Editor() { const initialConfig { editorState: null, namespace: Demo, nodes: [], onError: (error: Error) { throw error; }, theme: {}, }; const getDocFromMap (id: string, yjsDocMap: Mapstring, Y.Doc): Y.Doc { let doc yjsDocMap.get(id); if (doc undefined) { doc new Y.Doc(); yjsDocMap.set(id, doc); } else { doc.load(); } return doc; } const providerFactory useCallback( (id: string, yjsDocMap: Mapstring, Y.Doc) { const doc getDocFromMap(id, yjsDocMap); return new WebsocketProvider(ws://localhost:1234, id, doc, { connect: false, }); }, [], ); return ( LexicalCollaboration LexicalComposer initialConfig{initialConfig} RichTextPlugin contentEditable{ContentEditable classNameeditor-input /} placeholder{div classNameeditor-placeholderEnter some rich text.../div} ErrorBoundary{LexicalErrorBoundary} / CollaborationPlugin idlexical/react-rich-collab providerFactory{providerFactory} / /LexicalComposer /LexicalCollaboration ); }providerFactory中的ws://localhost:1234与前面启动y-websocket时使用的PORT1234对应id参数决定连接哪个 Yjs 房间需要与服务端引导文档所用的房间标识一致。文档默认使用 YjsDoc上名为root的XmlText共享类型作为根因此每个编辑器需要自己的文档。仅本地调试的替代路径可选如果只是跟着示例在本地玩文档允许给CollaborationPlugin添加下面两个 props 在客户端初始化状态// Dev-testing only, do not use in real-world cases. initialEditorState{$initialEditorState} shouldBootstrap{true}客户端的 bootstrap 逻辑只在根节点为空时写入初始状态root.isEmpty()判定并且文档源码useYjsCollaboration.tsx把 bootstrap 写入标记为非用户编辑、不进入 Yjs UndoManager 的栈。这条路径正是文档警告不要在生产使用的原因多个客户端各自判定为空并写入时就可能发生标题所述的并发创建冲突。验证结果服务端验证faq.md 同一段代码里提供了headlessConvertYDocStateToLexicalJSON它把一个 Y.Doc 状态Uint8Array通过applyUpdate应用到绑定上再把编辑器状态序列化为 Lexical JSON 返回。生成 bootstrappedY.Doc之后可以用这条链路确认文档中确实包含预期内容——返回的 JSON 里应能看到你写入的初始节点如示例中的初始段落而不是空文档。客户端验证provider 同步完成后editorState: null的编辑器由协作插件接管状态设置编辑器内容来自 Y.Doc 而非任何本地默认值。如果此时文档为空说明房间里的Y.Doc没有被正确引导需要回到服务端检查createBootstrappedYDoc的产出。边界与替代方案数据源选择faq.md Source of truth 一节建议把 Yjs 模型当作 source of truth数据库只做索引也可以让数据库当 source of truth其中文档给出一个更简单的变体客户端连接时若房间内容为空由服务端填充房间内容全部客户端断开超时后服务端忘掉房间内容。如果你的架构里已有服务端填充逻辑它可以与本文的服务端引导二选一但不要把客户端 bootstrap和服务端填充混用。provider 范围y-websocket是唯一官方支持的 Yjs 连接 provider更换 provider 时文档不做保证。完整参考实现文档指出的协作示例在仓库 examples/react-rich-collab可对照本文的依赖安装、y-websocket启动方式与编辑器配置。限制服务端如何把 bootstrappedY.Doc写入y-websocket的房间具体传输与服务框架由应用自身实现Lexical 文档只给出createBootstrappedYDoc这一产出物shouldBootstrapinitialEditorState的客户端初始化明确仅限开发测试。完成后的状态是房间中的Y.Doc在服务端就带着初始内容任意数量的客户端并发接入都只是同步同一份文档不再有客户端争抢初始化的路径服务端验证 JSON 与客户端同步后的编辑器内容应一致。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考