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

如何在服务端对持久化的TipTap(ProseMirror)文档数据应用事务?

如何在服务端对持久化的TipTap(ProseMirror)文档数据应用事务?

你这个思路完全没问题,而且这也是ProseMirror设计时就支持的场景——毕竟它本身就是为协作编辑这类需要增量更新的需求打造的。TipTap确实依赖DOM,但它只是ProseMirror的上层封装,核心的文档模型、事务处理都是纯JS实现的,完全可以脱离DOM在Node环境运行,不需要在服务端引入TipTap,直接用ProseMirror的核心包就行。

我给你梳理下具体的实现步骤:

1. 准备工作:确保前后端Schema一致

ProseMirror的文档结构完全依赖Schema,前后端必须用完全相同的Schema,否则序列化/解析JSON时会出错。你可以把Schema定义抽成一个共享文件,前端和服务端都导入它:

// shared/schema.js
const { Schema } = require('prosemirror-model');
const { nodes, marks } = require('prosemirror-schema-basic');

// 这里用ProseMirror基础Schema,和TipTap默认的Schema基本一致
// 如果你在TipTap里加了自定义节点/标记,也要同步加到这里
const docSchema = new Schema({
  nodes: { ...nodes },
  marks: { ...marks }
});

module.exports = docSchema;

2. 前端:捕获并发送事务

在TipTap的编辑器实例上,你可以通过onUpdate钩子捕获每次编辑产生的事务,然后把它序列化成JSON发送给服务端,记得带上当前文档的版本号(用来处理冲突):

editor.on('update', ({ transaction }) => {
  // 跳过无实际修改的事务(比如光标移动)
  if (transaction.docChanged) {
    const trJson = transaction.toJSON();
    // 发送到服务端接口
    fetch(`/api/docs/${docId}/apply-transaction`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        transaction: trJson,
        version: currentDocVersion // 从服务端获取的当前版本号
      })
    })
    .then(res => {
      if (res.status === 409) {
        // 版本冲突,需要重新拉取最新文档
        fetchLatestDoc();
      }
    });
  }
});

3. 服务端:加载旧文档、应用事务、持久化新文档

服务端只需要引入ProseMirror的prosemirror-model和prosemirror-transform包,然后按以下步骤处理:

const { Transaction } = require('prosemirror-transform');
const docSchema = require('../shared/schema');
const db = require('./db'); // 你的Postgres连接

async function applyTransaction(req, res) {
  const { docId } = req.params;
  const { transaction: trJson, version } = req.body;

  // 1. 从数据库加载旧文档和当前版本
  const result = await db.query(
    'SELECT content, version FROM docs WHERE id = $1',
    [docId]
  );
  if (result.rows.length === 0) {
    return res.status(404).send('Document not found');
  }
  const { content: oldDocJson, version: dbVersion } = result.rows[0];

  // 2. 检查版本是否匹配,处理冲突
  if (dbVersion !== version) {
    return res.status(409).send('Version conflict');
  }

  try {
    // 3. 解析旧文档和前端发来的事务
    const oldDoc = docSchema.nodeFromJSON(oldDocJson);
    const transaction = Transaction.fromJSON(docSchema, trJson);

    // 4. 应用事务得到新文档
    const newDoc = transaction.apply(oldDoc);

    // 5. 持久化新文档和更新版本号
    await db.query(
      'UPDATE docs SET content = $1, version = $2 WHERE id = $3',
      [newDoc.toJSON(), dbVersion + 1, docId]
    );

    res.status(200).send({ version: dbVersion + 1 });
  } catch (err) {
    console.error('Failed to apply transaction:', err);
    res.status(500).send('Internal server error');
  }
}

4. 额外注意事项

  • 事务顺序:事务是基于特定文档状态生成的,必须严格按顺序应用,如果前端有多个未确认的事务,最好等待前一个事务处理完成再发送下一个,或者在服务端排队处理。
  • 冲突处理:上面的版本号检查是乐观锁的基础,如果出现冲突,前端需要拉取最新文档,把未提交的事务重新应用到新文档上再发送(ProseMirror有map方法可以帮你调整事务到新的文档状态)。
  • 批量事务:如果觉得每次编辑都发请求太频繁,可以把多个事务合并成一个批次发送,服务端按顺序逐个应用就行。

这样就能实现增量更新,不用每次都发整个大文档了,完全符合你的需求。

备注:内容来源于stack exchange,提问作者Stephen Corwin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.23 07:43:18