【Tiptap】如何实现增量更新?

如果每次更新都将整个文档发给服务器,并保存,那对于大文档以及频繁更新的文档,这就太浪费了。既然Tiptap是以节点来标识文档,那做增量更新是容易的事情,具体有两种方式:

Implementing Efficient Updates in Tiptap: Sending Only Changes to the Server

Tiptap, built on ProseMirror, doesn't natively provide a built-in mechanism to automatically compute and serialize only the "delta" (changes) between document states for server-side patching. The standard approach via the onUpdate event sends the full document JSON on every change, which works for small documents but can be inefficient for large ones due to bandwidth and server load.

To send only changes, you have two main paths:

  1. Simple diffing of JSON states (client-side computation, suitable for non-collaborative, single-user editors). This involves tracking previous and current document JSON, computing a diff, and sending the delta to the server.
  2. Collaboration infrastructure with Y.js (recommended for robustness, especially if you might add real-time features later). Y.js handles operational transformation (OT) or conflict-free replicated data types (CRDTs) to track and sync changes natively, sending only deltas via a WebSocket server.

I'll outline both approaches with code examples. Assume you're using React (common for Tiptap), but these adapt easily to Vue/Svelte. You'll need to implement server-side logic to apply the changes (e.g., merge JSON diffs or apply Y.js updates).

Prerequisites
  • Install Tiptap: npm install @tiptap/react @tiptap/pm (core) + extensions as needed.
  • For diffing: npm install json-diff (lightweight JSON delta library).
  • For Y.js: npm install yjs y-websocket (client); server needs a WebSocket provider like y-websocket.
Approach 1: Client-Side JSON Diffing (Simple, No External Libs for Sync)

Track the previous document state and compute differences on update. Use json-diff to generate a patch (delta) object, which you send to the server via a PATCH request. The server reapplies the patch to its stored document.

This is lightweight but assumes single-user editing (concurrent changes could conflict; handle with versioning or locks).

Client-Side Code (React Example):

jsx 复制代码
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect, useRef } from 'react';
import diff from 'json-diff'; // For computing deltas

const TiptapEditor = ({ documentId }) => {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Initial content</p>', // Load initial from server
    onUpdate: ({ editor }) => {
      const currentJSON = editor.getJSON();
      const prevJSONRef = prevJSON.current;

      if (prevJSONRef) {
        // Compute diff (delta) between prev and current
        const delta = diff.diff(prevJSONRef, currentJSON);
        
        // Send only the delta to server (debounce if needed, e.g., via lodash.throttle)
        if (delta) {
          fetch(`/api/documents/${documentId}`, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ changes: delta }), // Delta is an array of { op: 'replace'|'add'|'remove', path: [...], value: ... }
          }).catch(console.error);
        }
      }

      // Update previous state
      prevJSON.current = currentJSON;
    },
  });

  const prevJSON = useRef(null);

  // Load initial document on mount
  useEffect(() => {
    fetch(`/api/documents/${documentId}`)
      .then(res => res.json())
      .then(data => {
        editor.commands.setContent(data.content); // Assuming server returns { content: JSON }
        prevJSON.current = data.content;
      });
  }, [documentId, editor]);

  if (!editor) return null;

  return <EditorContent editor={editor} />;
};

Key Notes:

  • onUpdate fires after every transaction (change). It provides editor for getJSON().

  • json-diff outputs a structured delta (e.g., [{ op: 'replace', path: ['content', 0, 'text'], value: 'new text' }]). Send this as the payload.

  • Debouncing: Wrap the fetch in a throttled function (e.g., 500ms delay) to avoid spamming on typing: Use lodash.throttle or a custom hook.

  • Edge Cases: Diffing nested ProseMirror JSON can be complex (e.g., node attributes). Test with your schema. For ranges, use ProseMirror's getChangedRanges(tr) from the transaction (onUpdate provides { transaction }).

  • Server-Side (Node.js Example with json-patch):

    js 复制代码
    const express = require('express');
    const jsonPatch = require('fast-json-patch');
    const app = express();
    
    // Assume documents stored in DB as JSON
    let docStore = { /* { id: { content: JSON } } */ };
    
    app.patch('/api/documents/:id', express.json(), (req, res) => {
      const id = req.params.id;
      const { changes } = req.body; // Delta from client
      const doc = docStore[id];
    
      if (!doc) return res.status(404).send('Not found');
    
      try {
        // Apply patch (json-diff deltas are compatible with json-patch)
        const patched = jsonPatch.applyPatch(doc.content, changes).newDocument;
        docStore[id].content = patched;
        // Save to DB
        res.send({ success: true });
      } catch (err) {
        res.status(400).send('Invalid patch');
      }
    });

    Install: npm install json-patch fast-json-patch.

Pros/Cons: Easy to implement, no WebSockets. But manual conflict resolution; not great for large docs or concurrency.

Approach 2: Y.js for Change Tracking and OT/CRDT Sync (Robust, Handles Concurrency)

Y.js is Tiptap's official collaboration backend. It treats the document as a stream of changes (not snapshots), so you naturally send only deltas. Updates are applied via operational transformation (OT) or CRDTs, merging concurrent edits automatically.

This requires a WebSocket server (e.g., Hocuspocus for Tiptap) but offloads diffing/sync to Y.js. Ideal if you plan multi-user support.

Setup:

  • Client: npm install @tiptap/extension-collaboration @tiptap/extension-collaboration-cursor yjs y-websocket.
  • Server: Use Hocuspocus (npm install @hocuspocus/server) for a simple WebSocket server that broadcasts Y.js updates.

Client-Side Code (React Example):

jsx 复制代码
import { useEditor, EditorContent } from '@tiptap/react';
import { Collaboration } from '@tiptap/extension-collaboration';
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'; // Optional for cursors
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import StarterKit from '@tiptap/starter-kit';

const TiptapEditor = ({ documentId, user }) => {
  const ydoc = new Y.Doc();
  const wsUrl = 'ws://localhost:1234'; // Your Hocuspocus server
  const connector = new WebsocketProvider(wsUrl, `document-${documentId}`, ydoc);

  const editor = useEditor({
    extensions: [
      StarterKit,
      // Bind Y.js as the source of truth
      Collaboration.configure({
        document: ydoc,
      }),
      // Optional: Show user cursors
      CollaborationCursor.configure({
        provider: connector,
        user: { name: user.name, color: user.color },
      }),
    ],
    content: '', // Y.js loads initial state
  });

  // Optional: Manual snapshot save (Y.js handles deltas automatically)
  useEffect(() => {
    const interval = setInterval(() => {
      // Get Y.js snapshot as JSON for backup/full save
      const json = Y.encodeStateAsUpdate(ydoc, Y.encodeStateVector(ydoc)); // Binary delta since last sync
      fetch(`/api/documents/${documentId}/snapshot`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/octet-stream' },
        body: json, // Send binary update (only changes)
      });
    }, 30000); // Every 30s

    return () => clearInterval(interval);
  }, [documentId, ydoc]);

  if (!editor) return null;

  return <EditorContent editor={editor} />;
};

Key Notes:

  • Y.js automatically syncs changes over WebSocket---only deltas are sent (binary updates via Y.encodeStateAsUpdate).

  • On first load, fetch the initial Y.js state from server (as binary or JSON via @hocuspocus/transformer).

  • onUpdate: Still available if needed, but Y.js handles propagation. Use editor.getJSON() for occasional full exports.

  • Server (Hocuspocus Example):

    js 复制代码
    const { Hocuspocus } = require('@hocuspocus/server');
    const server = Hocuspocus.createServer({
      port: 1234,
      // Persist Y.documents to DB for durability
      onStoreDocument: async (yDoc, documentName) => {
        // Save binary snapshot to DB
      },
      onLoadDocument: async (yDoc, documentName) => {
        // Load from DB
      },
    });
    server.listen();

    See Tiptap docs for full persistence setup.

Pros/Cons: Handles concurrency/OT out-of-the-box, efficient for large docs. Requires server setup.

Additional Tips
  • Versioning: Pair with Tiptap's Snapshot extension (@tiptap-pro/extension-snapshot) for manual snapshots and diffs (e.g., editor.commands.createSnapshot()).
  • Performance: For high-frequency updates, debounce with requestIdleCallback or libraries like use-debounce.
  • Testing Changes: Use ProseMirror's transaction in onUpdate to get raw steps: { editor, transaction }---transaction.steps are the atomic changes.
  • Docs References: Check Tiptap's Events for onUpdate and Collaboration for Y.js.

If your setup involves collaboration or custom nodes, go with Y.js. For simple cases, JSON diffing suffices. Provide more details (e.g., framework, schema) for tailored code!

相关推荐
HBR666_2 个月前
AI编辑器(FIM补全,AI扩写)简介
前端·ai·编辑器·fim·tiptap
HBR666_2 个月前
AI编辑器(二) ---调用模型的fim功能
前端·ai·编辑器·fim·tiptap
不老刘3 个月前
Tiptap(基于 Prosemirror)vs TinyMCE:哪个更适合你的技术栈?
编辑器·tinymce·tiptap·prosemirror