如何在NestJS后端从Yjs消息中保存Lexical编辑器内容?
从Yjs提取Lexical内容并在NestJS后端保存的实现方案
针对你已实现客户端实时协作、需要将内容持久化到数据库的场景,提供两种可行方案:
方案1:客户端提取Lexical内容后发送到后端
利用Lexical自身API序列化编辑器内容,直接发送至NestJS接口,适合手动触发保存(如用户点击保存按钮)或定时自动保存。
客户端代码示例(React环境)
import { $getRoot } from 'lexical'; import { LexicalEditor } from 'lexical'; // 提取编辑器内容并发送到后端 const saveToBackend = async (editor: LexicalEditor, docId: string) => { let serializedContent = null; // Lexical状态操作必须在update回调内执行 editor.update(() => { const rootNode = $getRoot(); // 序列化为Lexical标准JSON格式 serializedContent = rootNode.toJSON(); }); // 调用NestJS接口 await fetch(`/api/documents/${docId}/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: serializedContent }), }); };
NestJS后端接口示例
import { Controller, Post, Param, Body } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Document } from './document.entity'; // 假设你有Document实体 @Controller('api/documents') export class DocumentController { constructor( @InjectRepository(Document) private readonly docRepo: Repository<Document>, ) {} @Post(':id/save') async saveContent( @Param('id') docId: string, @Body() body: { content: Record<string, any> }, ) { await this.docRepo.save({ id: docId, content: body.content, updatedAt: new Date(), }); return { success: true }; } }
方案2:服务端从Yjs文档直接提取内容(实时自动备份)
利用服务端维护的Yjs文档实例,直接提取Lexical内容并保存,适合实时自动备份场景,无需客户端额外触发。
步骤1:NestJS中维护Yjs文档实例
import { Injectable } from '@nestjs/common'; import { Doc } from 'yjs'; @Injectable() export class YjsDocService { private docMap = new Map<string, Doc>(); private saveTimeout: NodeJS.Timeout | null = null; constructor(private readonly docSaveService: DocumentSaveService) {} getDocument(docId: string): Doc { if (!this.docMap.has(docId)) { const newDoc = new Doc(); // 监听Yjs文档变化,防抖后自动保存 newDoc.on('update', () => { if (this.saveTimeout) clearTimeout(this.saveTimeout); this.saveTimeout = setTimeout(() => { this.docSaveService.saveFromYjs(docId); }, 1000); // 1秒防抖,避免频繁写入 }); this.docMap.set(docId, newDoc); } return this.docMap.get(docId)!; } }
步骤2:从Yjs文档提取Lexical内容
import { Injectable } from '@nestjs/common'; import { YjsDocService } from './yjs-doc.service'; import { yjsToLexical } from '@lexical/yjs'; import { $getRoot, createEditor } from 'lexical'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Document } from './document.entity'; @Injectable() export class DocumentSaveService { constructor( private readonly yjsDocService: YjsDocService, @InjectRepository(Document) private readonly docRepo: Repository<Document>, ) {} async saveFromYjs(docId: string) { const yjsDoc = this.yjsDocService.getDocument(docId); // 获取Yjs中绑定的Lexical内容节点 const yLexicalFragment = yjsDoc.get('lexical', Y.XmlFragment); // 创建临时Lexical编辑器实例用于转换 const tempEditor = createEditor({ nodes: [], // 需与客户端编辑器的nodes配置完全一致 }); let serializedContent = null; await tempEditor.update(() => { // 将Yjs节点转换为Lexical节点 yjsToLexical(tempEditor, yLexicalFragment); const rootNode = $getRoot(); serializedContent = rootNode.toJSON(); }); // 保存到数据库 await this.docRepo.save({ id: docId, content: serializedContent, updatedAt: new Date(), }); // 销毁临时编辑器 tempEditor.destroy(); } }
关键注意事项
- 节点一致性:客户端和服务端的Lexical节点配置必须完全相同,否则会出现序列化/反序列化错误。
- 格式选择:除了保存Lexical JSON,还可以用
$generateHtmlFromNodes将内容转换为HTML后保存,适合需要直接展示的场景。 - 性能优化:服务端自动保存必须加防抖,避免Yjs频繁更新导致数据库压力过大。
- 数据一致性:如果需要支持离线编辑,需确保Yjs的更新日志与后端保存的版本同步,可通过Yjs的
encodeStateAsUpdate和applyUpdate实现版本控制。
内容的提问来源于stack exchange,提问作者Firas Latrach
相关产品推荐
相关产品推荐

