---
title: "比较文档版本"
description: "比较文档快照，查看两个版本之间的更改。"
canonical_url: "https://tiptap.zhcndoc.com/collaboration/documents/snapshot-compare"
---

# 比较文档版本

比较文档快照，查看两个版本之间的更改。

Snapshot Compare 允许你并排查看两个文档版本并突出显示所有更改。使用它来跟踪编辑，审查贡献，并恢复早期状态。

Snapshot Compare 扩展为 [快照](https://tiptap.zhcndoc.com/collaboration/documents/snapshot.md) 添加了额外的功能，允许你可视化比较两个版本之间的更改，以便跟踪添加、删除或修改的内容。这些比较称为 *diffs*。

- **1. Activate trial or subscribe**

  在您的账户中[开始免费试用](https://cloud.tiptap.dev/v2?trial=true)或[订阅团队计划](https://cloud.tiptap.dev/v2/billing)。
- **2. 启动文档服务器**

  在你的仪表板中 [添加环境](https://cloud.tiptap.dev/v2/configuration/document-server) 并配置你的 [文档服务器](https://cloud.tiptap.dev/v2/configuration/document-server)。
- **3. 从私有注册表安装**

  要安装此扩展，请按照 [设置指南](https://tiptap.zhcndoc.com/guides/pro-extensions.md) 进行身份验证以访问 Tiptap 的私有 npm 注册表。

> **Interactive demo:** [SnapshotCompare](https://embed-pro.tiptap.dev/preview/Extensions/SnapshotCompare)

## 访问私有注册表

Snapshot Compare 扩展已发布在 Tiptap 的私有 npm 注册表中。通过遵循 [私有注册表指南](https://tiptap.zhcndoc.com/guides/pro-extensions.md) 集成该扩展。如果你已经验证了你的 Tiptap 账号，可以直接进入 [#安装](#install)。

## 安装

从我们的私有注册表安装扩展：

```bash
npm install @tiptap-pro/extension-snapshot-compare
```

## 设置

你可以使用以下选项配置 `SnapshotCompare` 扩展：

| 设置                   | 类型                     | 默认值        | 描述                   |
| -------------------- | ---------------------- | ---------- | -------------------- |
| provider             | `TiptapCollabProvider` | `null`     | 协作提供者实例              |
| mapDiffToDecorations | `function`             | `() => {}` | 控制将 diff 映射到装饰以显示其内容 |

注意，你需要为 `TiptapCollabProvider` 提供 `user` 标识符，因为该信息用于优化 diffs。

```js
const provider = new TiptapCollabProvider({
  // ...
  user: 'your user identifier' // 必填！我们使用用户标识符来优化 diffs，因此提供它非常重要。
})

const editor = new Editor({
  // ...
  extensions: [
    // ...
    SnapshotCompare.configure({
      provider,
    }),
  ],
})
```

#### 使用 `mapDiffToDecorations` 进行 diff 装饰

该扩展具有默认映射 (`defaultMapDiffToDecorations`) 来表示 diffs 作为 ProseMirror 装饰。\
对于更复杂的集成和控制，你可以使用 `mapDiffToDecorations` 选项自定义此映射。

**示例：** 对内联插入应用自定义预定义背景颜色

```ts
SnapshotCompare.configure({
  mapDiffToDecorations: ({ diff, tr, editor, defaultMapDiffToDecorations }) => {
    if (diff.type === 'inline-insert') {
      // 返回 ProseMirror 装饰或 null
      return Decoration.inline(
        diff.from,
        diff.to,
        {
          class: 'diff',
          style: {
            backgroundColor: diff.attribution.color.backgroundColor,
          },
        },
        // 将 diff 作为装饰的规格传递，这对于 `extractAttributeChanges` 是必需的
        { diff },
      )
    }

    // 回退到默认映射
    return defaultMapDiffToDecorations({
      diff,
      tr,
      editor,
      attributes: {
        // 为装饰添加自定义属性
        'data-tiptap-user-id': myUserIdMapping[diff.attribution.userId],
      },
    })
  },
})
```

## 存储

`SnapshotCompare` 存储对象包含以下属性：

| 键               | 类型                    | 描述                  |
| --------------- | --------------------- | ------------------- |
| isPreviewing    | `boolean`             | 指示 diff 视图是否处于激活状态  |
| diffs           | `Diff[]`              | 在 diff 视图中显示的 diffs |
| previousContent | `JSONContent \| null` | 应用 diff 视图之前的内容     |

使用 `isPreviewing` 属性检查 diff 视图是否当前处于激活状态：

```ts
if (editor.storage.snapshotCompare.isPreviewing) {
  // diff 视图当前处于激活状态
}
```

使用 `diffs` 属性访问在 diff 视图中显示的 diffs：

```ts
editor.storage.snapshotCompare.diffs
```

属性 `previousContent` 由扩展内部使用，用于在退出 diff 视图时恢复内容。通常，你不需要直接与之交互。

## 命令

| 命令                | 描述                    |
| ----------------- | --------------------- |
| `compareVersions` | 比较两个版本并在编辑器中渲染 diff   |
| `showDiff`        | 给定更改跟踪变换，在编辑器中显示 diff |
| `hideDiff`        | 隐藏 diff 并恢复之前的内容      |

### compareVersions

使用 `compareVersions` 命令计算并显示两个文档版本之间的差异。

```ts
editor.chain().compareVersions({
  fromVersion: 1,
})
```

#### 选项

你可以传入额外的选项以更好地控制 diff 过程：

| 键                 | 描述                                            |
| ----------------- | --------------------------------------------- |
| `fromVersion`     | 比较的起始版本。`fromVersion` 和 `toVersion` 之间的顺序是灵活的 |
| `toVersion`       | 要比较的结束版本（默认：最新版本）                             |
| `hydrateUserData` | 向每个用户的更改添加上下文数据                               |
| `onCompare`       | 手动处理 diff 结果                                  |
| `enableDebugging` | 启用详细日志记录以进行故障排查                               |

#### 使用 `hydrateUserData` 添加元数据

每个 diff 具有 `attribution` 字段，允许你使用 `hydrateUserData` 回调函数添加附加元数据。

> **注意:**
>
> 请注意，`userId` 由 `TiptapCollabProvider` 填充，应该用于识别进行更改的用户。如果提供者未提供 `user` 字段，则 `userId` 将为 `null`。[有关更多信息，请参见 TiptapCollabProvider 文档](https://tiptap.zhcndoc.com/collaboration/provider/integration.md)。

**示例：** 基于用户为 diffs 着色

```ts
const colorMapping = new Map([
  ['user-1', '#ff0000'],
  ['user-2', '#00ff00'],
  ['user-3', '#0000ff'],
])

editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  hydrateUserData: ({ userId }) => {
    return {
      color: {
        backgroundColor: colorMapping.get(userId),
      },
    }
  },
})

editor.storage.snapshotCompare.diffs[0].attribution.color.backgroundColor // '#ff0000'
```

#### 使用 `onCompare` 自定义 diff 过程

如果你需要更好地控制 diff 过程，可以使用 `onCompare` 选项接收结果并自行处理。

**示例：** 按用户过滤 diffs

```ts
editor.chain().compareVersions({
  fromVersion: 1,
  toVersion: 3,
  onCompare: (ctx) => {
    if (ctx.error) {
      // 处理 diff 过程中的错误
      console.error(ctx.error)
      return
    }

    // 过滤 diffs 以仅显示特定用户的更改
    const diffsToDisplay = ctx.diffSet.filter((diff) => diff.attribution.userId === 'user-1')

    editor.commands.showDiff(ctx.tr, { diffs: diffsToDisplay })
  },
})
```

### showDiff

使用 `showDiff` 命令在编辑器中显示 diff，使用更改跟踪变换 (`tr`)。这代表自上一个快照以来对文档所做的所有更改。你可以使用此变换在编辑器中显示 diff。

通常，在使用 `compareVersions`、`onCompare` 自定义或过滤 diffs 后使用此命令。

`showDiff` 命令临时替换当前编辑器内容为 diff 视图，显示版本之间的差异。它还会缓存当前在编辑器中显示的内容，以便你稍后可以恢复它。

```ts
// 这将显示在编辑器中记录的更改跟踪变换的变更
editor.commands.showDiff(tr)
```

### 选项

你可以传入额外的选项以控制如何显示 diffs：

| 键     | 描述               |
| ----- | ---------------- |
| diffs | 一个要可视化的 diffs 数组 |

**示例：** 显示特定的 diffs

```ts
// 这将仅显示由 ID 为 'user-1' 的用户所做的 diffs
const diffsToDisplay = tr.toDiff().filter((diff) => diff.attribution.userId === 'user-1')

editor.commands.showDiff(tr, { diffs: diffsToDisplay })
```

### hideDiff

使用 `hideDiff` 命令隐藏 diff 并恢复之前的内容。

```ts
// 这将隐藏 diff 视图并恢复之前的内容
editor.commands.hideDiff()
```

## 添加样式

Snapshot Compare 扩展为 diff 视图的元素应用了类，以帮助你为插入及删除的文本设置样式。请参见本页面上方的[代码示例](#page-title)中的完整示例，了解如何为 diff 视图设置样式。

### 基础 diff 样式

扩展为 diff 元素添加了以下属性：

- `data-diff-type="inline-insert"` - 插入的文本
- `data-diff-type="inline-delete"` - 删除的文本
- `data-diff-type="inline-update"` - 更新的文本
- `data-diff-type="block-insert"` - 插入的块
- `data-diff-type="block-delete"` - 删除的块

你可以通过 CSS 选择器针对这些带有数据属性的元素进行样式设置。

下面是插入和删除文本的基础样式示例：

```css
/* 当没有用户信息时，退回到红色和绿色的颜色 */

/* 样式插入的文本 */
[data-diff-type='inline-insert'],
[data-diff-type='inline-update'],
[data-diff-type='block-insert'] {
  background-color: green;
}

/* 样式删除的文本 */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  background-color: red;
  text-decoration: line-through;
}
```

### 重置删除文本的标记样式

当应用了样式标记（比如 **粗体**、*斜体* 或 `代码`）时，删除的文本（即应用格式前的文本）会继承这些格式。这是因为删除的文本出现在相应HTML标签内部。为避免此问题，请在删除内容中重置这些标记样式。

首先，在删除的内容内部重置外层标记样式。

```scss
/* 为删除内容重置粗体/strong 样式 */
strong {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-weight: normal;
  }
}

/* 为删除内容重置斜体/em 样式 */
em {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-style: normal;
  }
}

/* 为删除内容重置代码/code 样式 */
code {
  [data-diff-type='inline-delete'],
  [data-diff-type='block-delete'] {
    font-family: sans-serif;
  }
}
```

然后，如果删除内容内部有标记标签，则重新应用这些标记样式，确保格式正确应用。

```scss
/* 确保删除内容内的标记样式正常工作 */
[data-diff-type='inline-delete'],
[data-diff-type='block-delete'] {
  strong {
    font-weight: bold;
  }
  em {
    font-style: italic;
  }
  code {
    font-family: monospace;
  }
}
```

### 用户归属样式

当有用户归属信息时，你可以根据进行更改的用户来为 diffs 设置样式：

```css
/* 带用户归属的 diffs 样式 */
[data-diff-user-id] {
  position: relative;
}

/* 用于显示用户名的提示信息 */
[data-diff-user-id]::before {
  content: attr(data-diff-user-id);
  position: absolute;
  visibility: hidden;
}

[data-diff-user-id]:hover::before {
  visibility: visible;
}
```

## 使用 NodeView （高级）

在使用 [自定义节点视图](https://tiptap.zhcndoc.com/editor/extensions/custom-extensions/node-views.md) 时，默认的 diff 映射可能无法按预期工作。你可以自定义映射并直接在自定义节点视图中渲染 diffs。

使用 `extractAttributeChanges` 助手提取节点中的属性更改。这使你能够访问节点的先前和当前属性，从而可能突出显示自定义节点视图中的属性更改。

> **注意:**
>
> 当将 diffs 映射到装饰时，你需要将 `diff` 作为装饰的 `spec` 传递。这对于 `extractAttributeChanges` 的正常工作是必需的。

**示例：** 自定义标题节点视图以显示更改

```ts
import { extractAttributeChanges } from '@tiptap-pro/extension-snapshot-compare'

const Heading = BaseHeading.extend({
  addNodeView() {
    return ReactNodeViewRenderer(({ node, decorations }) => {
      const { before, after, isDiffing } = extractAttributeChanges(decorations)

      return (
        <NodeViewWrapper style={{ position: 'relative' }}>
          {isDiffing && before.level !== after.level && (
            <span
              style={{
                position: 'absolute',
                right: '100%',
                fontSize: '14px',
                color: '#999',
                backgroundColor: '#f0f0f070',
              }}
              // 显示级别属性的变化
            >
              #<s>{before.level}</s>
              {after.level}
            </span>
          )}
          <NodeViewContent as={`h${node.attrs.level ?? 1}`} />
        </NodeViewWrapper>
      )
    })
  },
})
```

## 技术细节

### Diff

`Diff` 是一个表示文档中所做更改的对象。它包含以下属性：

| 属性                    | 类型                                                                                                                      | 描述                                                            |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `type`                | `'inline-insert'` \| `'inline-delete'` \| `'block-insert'` \| `'block-delete'` \| `'inline-update'` \| `'block-update'` | 所做更改的类型                                                       |
| `from`                | `number`                                                                                                                | 更改的起始位置                                                       |
| `to`                  | `number`                                                                                                                | 更改的结束位置                                                       |
| `content`             | `Fragment`                                                                                                              | 添加或删除的内容                                                      |
| `attribution`         | `Attribution`                                                                                                           | 有关更改的元数据，例如执行更改的用户                                            |
| `attributes?`         | `Record<string, any>`                                                                                                   | 更改 **后** 的属性；仅对 `'inline-update'` 和 `'block-update'` diffs 可用 |
| `previousAttributes?` | `Record<string, any>`                                                                                                   | 更改 **前** 的属性；仅对 `'inline-update'` 和 `'block-update'` diffs 可用 |

### DiffSet

`DiffSet` 是一个 `Diff` 对象数组，每个对象对应于特定的更改，例如插入、删除或更新。你可以迭代数组以检查单个更改或基于 diff 类型应用自定义逻辑。

```ts
const diffsToDisplay = diffSet.filter((diff) => diff.attribution.userId === 'user-1')

// 在编辑器中显示过滤后的 diffs
editor.commands.showDiff(tr, { diffs: diffsToDisplay })
```

### Attribution

`Attribution` 对象包含有关更改的元数据。它包括以下属性：

| 属性       | 类型                       | 描述                   |
| -------- | ------------------------ | -------------------- |
| `type`   | `'added'` \| `'removed'` | 指示所做更改的类型            |
| `userId` | `string` \| `undefined`  | 执行更改的用户的 ID          |
| `id`     | `Y.ID` \| `undefined`    | 执行更改的用户的 Y.js 客户端 ID |

你可以扩展 `Attribution` 接口以包括其他属性：

```ts
declare module '@tiptap-pro/extension-snapshot-compare' {
  interface Attribution {
    userName: string
  }
}
```

### ChangeTrackingTransform

`ChangeTrackingTransform` 是一个记录对文档所做更改的类（基于 ProseMirror 的 `Transform`）。\
它表示一个变换，其步骤描述了从文档的一个版本到另一个版本所做的所有更改。它具有以下属性：

| 属性       | 类型                     | 描述             |
| -------- | ---------------------- | -------------- |
| `steps`  | `ChangeTrackingStep[]` | 表示对文档所做更改的步骤数组 |
| `doc`    | `Node`                 | 应用更改后文档的状态     |
| `before` | `Node`                 | 应用更改前文档的状态     |

### ChangeTrackingStep

`ChangeTrackingStep` 是一个基于 ProseMirror 的 `ReplaceStep` 类表示的类，代表对文档所做的单更改。它具有以下属性：

| 属性            | 类型            | 描述                 |
| ------------- | ------------- | ------------------ |
| `attribution` | `Attribution` | 有关更改的元数据，例如执行更改的用户 |

### 类型

以下是 `SnapshotCompare` 扩展的完整 TypeScript 定义：

```ts
declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    snapshotCompare: {
      /**
       * 给定更改跟踪变换，在编辑器内显示 diff
       */
      showDiff: (tr: ChangeTrackingTransform, options?: { diffs?: DiffSet }) => ReturnType
      /**
       * 隐藏 diff 并恢复之前的内容
       */
      hideDiff: () => ReturnType
      /**
       * 比较两个版本并将 diff 渲染到编辑器中
       */
      compareVersions: <
        T extends Pick<Attribution, 'color'> & Record<string, any> = Pick<Attribution, 'color'> &
          Record<string, any>,
      >(options: {
        /**
         * 开始 diff 的版本
         */
        fromVersion: number
        /**
         * 结束 diff 的版本
         * 如果未提供，将使用最新快照
         */
        toVersion?: number
        /**
         * 允许为每个用户的更改添加上下文数据
         */
        hydrateUserData?: (context: {
          /**
           * 事件的类型
           */
          type: 'added' | 'removed'
          /**
           * 操作此内容的 userId
           */
          userId: string | undefined
          /**
           * 内容的 yjs 标识符
           */
          id?: y.ID
        }) => T
        /**
         * 如果提供，允许自定义渲染 diffs 到编辑器的行为。
         * @note 默认行为是立即显示 diff。
         */
        onCompare?: (
          context:
            | {
                error?: undefined
                /**
                 * 编辑器实例
                 */
                editor: Editor
                /**
                 * 带有归属元数据的变换所有更改
                 */
                tr: ChangeTrackingTransform<Attribution & T>
                /**
                 * 作为 diffs 数组表示的更改
                 */
                diffSet: DiffSet<Attribution & T>
              }
            | {
                error: Error
              },
        ) => void
        /**
         * 详细记录 diffing 过程以帮助追踪错误
         */
        enableDebugging?: boolean
      }) => ReturnType
    }
  }
}

export type SnapshotCompareOptions = {
  /**
   * tiptap 提供者实例。这对于扩展示计算 diffs 是必需的，但不是必需的以显示它们。
   * 也可以传递 TiptapCollabProvider 实例。
   */
  provider: TiptapCollabProvider | null
  /**
   * 这允许你控制将 diff 映射到装饰以显示该 diff 内容的方式
   */
  mapDiffToDecorations?: (options: {
    /**
     * 要映射到装饰的 diff
     */
    diff: Diff
    /**
     * 编辑器实例
     */
    editor: Editor
    /**
     * 更改跟踪变换
     */
    tr: ChangeTrackingTransform
    /**
     * 将 diff 映射到装饰的默认实现
     */
    defaultMapDiffToDecorations: typeof defaultMapDiffToDecorations
  }) => ReturnType<typeof defaultMapDiffToDecorations>
}

export type SnapshotCompareStorageInactive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: false
  /**
   * diff 视图应用之前的内容
   */
  previousContent: null
  /**
   * 已应用的更改跟踪变换
   * 因为 diff 视图没有激活，它当前是空的
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: null
}

export type SnapshotCompareStorageActive = {
  /**
   * diff 视图当前是否处于激活状态
   */
  isPreviewing: true
  /**
   * diff 视图应用之前的内容
   */
  previousContent: JSONContent
  /**
   * 已应用的更改跟踪变换
   */
  diffs: DiffSet
  /**
   * 已应用的更改跟踪变换
   */
  tr: ChangeTrackingTransform
}

export type SnapshotCompareStorage = SnapshotCompareStorageInactive | SnapshotCompareStorageActive
```
