Report an issue

指南将修订历史与您的应用程序集成

修订历史插件 提供一个 API,让您能够创建和管理文档的命名修订。要将修订保存到您的数据库并访问它们,您首先需要集成此功能。

本指南介绍如何将修订历史功能作为独立(异步)插件进行集成。

如果您使用的是实时协作功能,请参考 实时协作功能集成 指南。

# 集成方法

本指南将讨论两种将 CKEditor 5 与修订历史插件集成的途径

适配器集成是推荐的方法,有两个原因

  • 它让您更好地控制数据。
  • 它更有效,并提供更好的用户体验,因为修订的数据按需加载,而不是在编辑器初始化之前提前加载。

# 开始之前

作为本指南的补充,我们提供了 可供下载的即用型样本。您可以将样本作为示例或作为您自己的集成的起点。

# 准备自定义编辑器设置

要使用修订历史插件,您需要准备一个包含修订历史功能异步版本的自定义编辑器设置。

最简单的方法是使用 构建器。选择一个预设,然后开始自定义您的编辑器。

构建器允许您选择首选的发布方法和框架。在本指南中,我们将使用“Vanilla JS”选项,使用“npm”和一个基于“Classic Editor (basic)”预设的简单设置,以及启用的修订历史功能。

在构建器的“功能”部分(第二步),请确保

  • 启用“协作 → 修订历史”功能,
  • 关闭“协作”组旁边的“实时”切换。

为了获得更好的演示体验,我们建议您启用更多功能 - 整个“文本格式”组和“图像 → 块图像”功能。

完成设置后,构建器将为您提供必要的 HTML、CSS 和 JavaScript 代码片段。我们将在下一步使用这些代码片段。

# 设置示例项目

设置好自定义编辑器后,我们需要一个简单的 JavaScript 项目来运行它。为此,我们建议您从我们的仓库克隆基本项目模板

npx -y degit ckeditor/ckeditor5-tutorials-examples/sample-project sample-project
cd sample-project
npm install

然后,安装必要的依赖项

npm install ckeditor5
npm install ckeditor5-premium-features

此项目模板在后台使用 Vite,并包含我们将使用的 3 个源文件:index.htmlstyle.cssmain.js

现在是使用自定义编辑器设置的时候了。转到构建器的“安装”部分,并将生成的代码片段复制到这 3 个文件中。

# 激活功能

要使用此高级功能,您需要使用许可证密钥激活它。有关详细信息,请参阅 许可证密钥和激活 指南。

成功获取许可证密钥后,打开 index.js 文件,并将 your-license-key 字符串更新为您的许可证密钥。

# 构建项目

最后,通过运行以下命令构建项目

npm run dev

在浏览器中打开示例时,您应该看到带有修订历史插件的 WYSIWYG 编辑器。但是,它仍然不加载或保存任何数据。您将在本指南的后面学习如何将数据添加到评论插件中。

现在让我们深入了解此设置的结构。

# 基本设置的解剖

现在让我们浏览此基本设置的关键片段。

# HTML 结构

页面的 HTML 和 CSS 结构创建了两个主要容器

  • <div class="editor-container__editor"> 是编辑器使用的主要容器。
  • <div class="revision-history"> 是修订历史使用的主要容器。
  • <div class="revision-history__editor"> 是修订历史使用的容器,显示选定修订的内容。
  • <div class="revision-history__sidebar"> 是修订历史侧边栏使用的容器,包含修订列表。

# JavaScript

main.js 文件设置编辑器实例

  • revisionHistory 按钮添加到编辑器工具栏。
  • 加载所有必要的编辑器插件(包括 RevisionHistory 插件)。
  • 设置 licenseKey 配置选项。
  • 设置 revisionHistory 配置选项以指向上面提到的容器。
  • 定义我们将在本教程的后续步骤中使用的 RevisionHistoryIntegrationUsersIntegrations 插件的模板。

# 修订历史 API

以下集成使用修订历史 API。熟悉 API 可能有助于您理解代码片段。如果遇到任何问题,请参阅 修订历史 API 文档

# 下一步

我们已经设置了一个简单的 JavaScript 项目,它运行一个带有修订历史功能的异步版本的 CKEditor 实例。但是,它还没有处理加载或保存数据。接下来的两节介绍了两种可用的集成方法。

# 一个简单的“加载和保存”集成

在此解决方案中,您在编辑器初始化期间加载用户和修订数据。您在完成编辑器操作后保存修订数据(例如,当您提交包含 WYSIWYG 编辑器的表单时)。

# 加载数据

在您将修订历史插件包含在编辑器中之后,您需要创建一个插件来初始化用户和现有修订。

您应该在插件初始化步骤中加载修订数据,即使用您将提供的集成插件的 init() 方法(如以下示例)。

首先,将用户和修订数据存储到一个变量中,该变量将可供您的插件使用。

// Revisions data will be available under a global variable `revisions`.
const revisions = [
    {
        "id": "initial",
        "name": "Initial revision",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
            }
        },
        "createdAt": "2024-05-27T13:22:59.077Z",
        "attributes": {},
        "fromVersion": 1,
        "toVersion": 1
    },
    {
        "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
        "name": "Updated with the actual data",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]'
            }
        },
        "createdAt": "2024-05-27T13:23:52.553Z",
        "attributes": {},
        "fromVersion": 1,
        "toVersion": 20
    },
    {
        "id": "e6590c50ccbc86acacb7d27231ad32064",
        "name": "Inserted logo",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.npmjs.net.cn/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
            }
        },
        "createdAt": "2024-05-27T13:26:39.252Z",
        "attributes": {},
        "fromVersion": 20,
        "toVersion": 24
    },
    // An empty current revision.
    {
        "id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
        "name": null,
        "creatorId": null,
        "authorsIds": [],
        "diffData": {
            "main": {
                "insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.npmjs.net.cn/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
            }
        },
        "createdAt": "2024-05-27T13:26:39.252Z",
        "attributes": {},
        "fromVersion": 24,
        "toVersion": 24
    }
];

然后,修改 RevisionHistoryIntegration 插件以从 revisions 数组中读取数据,并使用 RevisionsRepository API 将其加载到编辑器中。

如果您的应用程序需要异步地从服务器请求修订数据,您可以创建一个插件,从数据库中获取数据,而不是将数据放在 HTML 源代码中。在这种情况下,您的插件应该 Plugin.init 方法返回一个 Promise,以确保编辑器初始化等待您的数据。

您还可以参考 适配器集成 部分中显示的示例。

class RevisionHistoryIntegration extends Plugin {
    static get pluginName() {
        return 'RevisionHistoryIntegration';
    }

    static get requires() {
        return [ 'RevisionHistory' ];
    }

    init() {
        const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );

        for ( const revisionData of revisions ) {
            revisionHistory.addRevisionData( revisionData );
        }
    }
}

由于将使用修订数据,因此不再需要 editorConfig.initialData,您可以将其删除,如 编辑器初始数据和修订数据 中所述。

# 保存数据

您必须使文档数据和修订数据保持同步,才能使该功能正常工作。

您必须始终将修订数据和文档数据一起保存。

要保存修订数据,您需要先从 RevisionsRepository 插件中获取它。为此,请使用 getRevisions() 方法。

然后,使用该数据以您选择的方式将其保存在您的数据库中。请参阅以下示例。请记住更新您的 HTML 结构以包含一个具有 get-data ID 的按钮,例如 <button id="get-data">获取编辑器数据</button>

ClassicEditor
    .create(document.querySelector('#editor'), editorConfig)
    .then( editor => {
        // After the editor is initialized, add an action to be performed after a button is clicked.
        document.querySelector( '#get-data' ).addEventListener( 'click', () => {
            const revisionHistory = editor.plugins.get( 'RevisionHistory' );

            // Get the document data and the revisions data (in JSON format, so it is easier to save).
            const editorData = editor.data.get();
            const revisionsData = revisionHistory.getRevisions( { toJSON: true } );

            // Now, use `editorData` and `revisionsData` to save the data in your application.
            //
            // Note: it is a good idea to verify the revision `creatorId` parameter when saving
            // a revision in the database. However, do not overwrite the value if it was set to `null`!
            console.log( editorData );
            console.log( revisionsData );
        } );
    } )
    .catch( error => console.error( error ) );

建议将修订的 attributes 属性值的字符串转换为 JSON,并将其作为字符串保存在您的数据库中。然后,在加载修订时从 JSON 解析该值。

现在,该集成已准备好与您的富文本编辑器一起使用。

# 演示

控制台

// Use the `Save revisions` button to see the result...

# 适配器集成

适配器集成使用您提供的适配器对象,将修订数据立即保存到您的数据存储中。

这是将修订历史与您的应用程序集成的推荐方法,因为它允许您更安全地处理客户端 - 服务器通信。例如,您可以检查用户权限、验证发送的数据或使用在服务器端获得的信息更新数据。

此外,修订可能包含大量数据。加载大型文档的多个修订可能会对您的应用程序的加载时间产生不利影响。使用适配器时,修订数据是按需加载的,在需要时加载。这将改善整体用户体验。

在插件初始化步骤中加载修订数据非常重要,即使用您将提供的集成插件的 init() 方法(如以下示例)。

此示例不包含评论和跟踪更改适配器。查看 评论集成 指南和 跟踪更改集成 指南,了解如何构建完整的解决方案。此外,这些代码片段定义了相同的用户列表。确保对该代码进行重复数据删除,并仅定义一次用户列表,以避免错误。

# 保存文档数据和修订数据

在进入实际实现之前,请记住,您应该始终使文档数据和修订数据保持同步。这意味着,每当您保存一个时,也应该保存另一个。对于“加载和保存”集成来说这是很自然的,但对于适配器集成,您需要牢记这一点。数据不匹配会导致该功能无法正常工作。

此外,在保存文档数据后,不应对其进行进一步的后处理。更准确地说,不应以在加载文档数据后会导致不同模型的方式更改它。

# 实现

首先,使用 RevisionHistory#adapter 属性定义适配器。适配器方法允许您加载和保存数据库中的更改。仔细阅读 RevisionHistoryAdapter 的 API 参考,以确保您将该功能与您的应用程序正确集成。

修订中的每个更改都会立即在 UI 端执行。但是,所有适配器操作都是异步的,并在后台执行。因此,所有适配器方法都需要返回一个 Promise。当 promise 解析时,表示一切顺利,本地更改已成功保存到数据存储中。当 promise 被拒绝时,编辑器会抛出一个 CKEditorError 错误,该错误与 监视程序 功能一起很好地工作。在处理服务器响应时,您可以决定是解析 promise 还是拒绝 promise。

在适配器正在保存修订数据时,一个挂起操作会自动添加到编辑器的 PendingActions 插件中。因此,您不必担心在适配器操作完成之前编辑器会被销毁。

现在您已经准备好创建适配器。让我们从模拟修订数据开始。在实际场景中,这将是存储在您的数据存储中的数据。

// Revisions data will be available under a global variable `revisions`.
const revisions = [
    {
        "id": "initial",
        "name": "Initial revision",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ………… by and between The Lower Shelf, the “Publisher”, and …………, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him/herself and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A."]}]'
            }
        },
        "createdAt": "2024-05-27T13:22:59.077Z",
        "attributes": {},
        "fromVersion": 1,
        "toVersion": 1
    },
    {
        "id": "e6f80e6be6ee6057fd5a449ab13fba25d",
        "name": "Updated with the actual data",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","insertion:user-1:0"]],"children":[]},"1st",{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]}," ",{"name":"revision-start","attributes":[["name","insertion:user-1:1"]],"children":[]},"June 2020 ",{"name":"revision-end","attributes":[["name","insertion:user-1:1"]],"children":[]},"by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","insertion:user-1:2"]],"children":[]},"John Smith",{"name":"revision-end","attributes":[["name","insertion:user-1:2"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of ",{"name":"revision-start","attributes":[["name","deletion:user-1:0"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]}," by and between The Lower Shelf, the “Publisher”, and ",{"name":"revision-start","attributes":[["name","deletion:user-1:1"]],"children":[]},"…………",{"name":"revision-end","attributes":[["name","deletion:user-1:1"]],"children":[]},", the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him",{"name":"revision-start","attributes":[["name","deletion:user-1:2"]],"children":[]},"/herself",{"name":"revision-end","attributes":[["name","deletion:user-1:2"]],"children":[]}," and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature.",{"name":"revision-start","attributes":[["name","deletion:user-1:3"]],"children":[]}]},{"name":"p","attributes":[],"children":["Publishing formats are enumerated in Appendix A.",{"name":"revision-end","attributes":[["name","deletion:user-1:3"]],"children":[]}]}]'
            }
        },
        "createdAt": "2024-05-27T13:23:52.553Z",
        "attributes": {},
        "fromVersion": 1,
        "toVersion": 20
    },
    {
        "id": "e6590c50ccbc86acacb7d27231ad32064",
        "name": "Inserted logo",
        "creatorId": "user-1",
        "authorsIds": [ "user-1" ],
        "diffData": {
            "main": {
                "insertions": '[{"name":"figure","attributes":[["data-revision-start-before","insertion:user-1:0"],["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.npmjs.net.cn/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":[{"name":"revision-end","attributes":[["name","insertion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[["data-revision-start-before","deletion:user-1:0"]],"children":[{"name":"revision-end","attributes":[["name","deletion:user-1:0"]],"children":[]},"PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
            }
        },
        "createdAt": "2024-05-27T13:26:39.252Z",
        "attributes": {},
        "fromVersion": 20,
        "toVersion": 24
    },
    // An empty current revision.
    {
        "id": "egh91t5jccbi894cacxx7dz7t36aj3k021",
        "name": null,
        "creatorId": null,
        "authorsIds": [],
        "diffData": {
            "main": {
                "insertions": '[{"name":"figure","attributes":[["class","image"]],"children":[{"name":"img","attributes":[["src","https://ckeditor.npmjs.net.cn/docs/ckeditor5/latest/assets/img/revision-history-demo.png"]],"children":[]}]},{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]',
                "deletions": '[{"name":"h1","attributes":[],"children":["PUBLISHING AGREEMENT"]},{"name":"h3","attributes":[],"children":["Introduction"]},{"name":"p","attributes":[],"children":["This publishing contract, the “contract”, is entered into as of 1st June 2020 by and between The Lower Shelf, the “Publisher”, and John Smith, the “Author”."]},{"name":"h3","attributes":[],"children":["Grant of Rights"]},{"name":"p","attributes":[],"children":["The Author grants the Publisher full right and title to the following, in perpetuity:"]},{"name":"ul","attributes":[],"children":[{"name":"li","attributes":[],"children":["To publish, sell, and profit from the listed works in all languages and formats in existence today and at any point in the future."]},{"name":"li","attributes":[],"children":["To create or devise modified, abridged, or derivative works based on the works listed."]},{"name":"li","attributes":[],"children":["To allow others to use the listed works at their discretion, without providing additional compensation to the Author."]}]},{"name":"p","attributes":[],"children":["These rights are granted by the Author on behalf of him and their successors, heirs, executors, and any other party who may attempt to lay claim to these rights at any point now or in the future."]},{"name":"p","attributes":[],"children":["Any rights not granted to the Publisher above remain with the Author."]},{"name":"p","attributes":[],"children":["The rights granted to the Publisher by the Author shall not be constrained by geographic territories and are considered global in nature."]}]'
            }
        },
        "createdAt": "2024-05-27T13:26:39.252Z",
        "attributes": {},
        "fromVersion": 24,
        "toVersion": 24
    }
];

接下来,根据 RevisionHistoryIntegration 插件模板创建适配器插件,以异步地获取修订数据。

// A plugin that introduces the adapter.
class RevisionHistoryIntegration extends Plugin {
    static get pluginName() {
        return 'RevisionHistoryIntegration';
    }

    static get requires() {
        return [ 'RevisionHistory' ];
    }

    async init() {
        const revisionHistory = this.editor.plugins.get( 'RevisionHistory' );

        revisionHistory.adapter = {
            getRevision: ( { revisionId } ) => {
                return this._findRevision( revisionId );
            },
            updateRevisions: revisionsData => {
                const documentData = this.editor.getData();

                // This should be an asynchronous request to your database
                // that saves `revisionsData` and `documentData`.
                //
                // The document data should be saved each time a revision is saved.
                //
                // `revisionsData` is an array with objects,
                // where each object contains updated and new revisions.
                //
                // See the API reference for `RevisionHistoryAdapter` to learn
                // how to correctly integrate the feature with your application.
                //
                return Promise.resolve();
            }
        };

        // Add the revisions data for existing revisions.
        const revisionsData = await this._fetchRevisionsData();

        for ( const revisionData of revisionsData ) {
            revisionHistory.addRevisionData( revisionData );
        }
    }

    async _findRevision( revisionId ) {
        // Get the revision data based on its ID.
        // This should be an asynchronous request to your database.
        return Promise.resolve( revisions.find( revision => revision.id === revisionId ) );
    }

    async _fetchRevisionsData() {
        // Get a list of all revisions.
        // This should be an asynchronous call to your database.
        //
        // Note that the revision list should not contain the `diffData` property.
        // The `diffData` property may be big and will be fetched on demand by `adapter.getRevision()`.
        return Promise.resolve(revisions.map(revision => ({ ...revision, diffData: undefined })));
    }
}

由于将使用修订数据,因此不再需要 editorConfig.initialData,您可以将其删除,如 编辑器初始数据和修订数据 中所述。

建议将修订的 attributes 属性值的字符串转换为 JSON,并将其作为字符串保存在您的数据库中。然后,在加载修订时从 JSON 解析该值。

现在,该适配器已准备好与您的富文本编辑器一起使用。

# 演示

修订历史适配器操作控制台

// Create new version to see the result...

# 了解详情

# 挂起操作

修订历史使用 挂起操作 功能。当修订数据通过修订历史适配器更新时,会添加挂起操作,以防止在更新完成之前关闭编辑器。

# 编辑器初始数据和修订数据

当使用修订历史功能时,使修订状态与文档数据保持同步至关重要。因此,如果为文档保存了任何修订,编辑器初始数据将被丢弃。将使用保存到最新修订的 data 替换它。

由于编辑器的初始数据被丢弃,因此您可以通过将文档的初始数据设置为为空来降低数据负载。这适用于已创建至少一次修订的文档。

如果编辑器的初始数据已设置,并且与修订数据不同,则将在控制台中记录一个警告。

# 自动保存集成

如果您使用的是实时协作功能,请参阅 实时协作功能集成指南 中的 自动保存修订历史 部分。

本节介绍如何将修订历史与自动保存插件集成。这样,您可以经常保存您的文档和修订数据,以使其保持同步。

虽然本指南提供了可直接使用的代码片段,但我们鼓励您也阅读 修订是如何保存和更新的 部分,以更好地了解此主题。

当启用自动保存插件并打开修订历史时,将调用自动保存回调。

集成在您是否使用适配器方面略有不同。

# 自动保存和“加载和保存”集成

更新修订,并确保将更新的或创建的修订与编辑器数据一起保存

autosave: {
    save: async editor => {
        const revisionTracker = editor.plugins.get( 'RevisionTracker' );

        await revisionTracker.update();

        const revisionData = revisionTracker.currentRevision.toJSON();
        const documentData = editor.getData();

        // `saveData()` should save the document and revision data in your database
        // and return a `Promise` that resolves when the save is completed.
        return saveData( documentData, revisionData );
    }
}

# 自动保存与适配器集成

使用适配器时,集成更容易。您的修订适配器也应该保存文档数据,如之前的示例中所示。

由于适配器已经负责保存修订数据和文档数据,所以在自动保存集成中只需要更新修订。

autosave: {
    save: editor => {
        const revisionTracker = editor.plugins.get( 'RevisionTracker' );

        return revisionTracker.update();
    }
}

# 高级自动保存策略

所展示的集成将持续更新同一个修订,直到用户显式保存或命名当前修订,或关闭编辑器。

这可能导致创建包含大量更改的大修订。为了防止这种情况,自动保存集成可以根据您的自定义策略创建新的修订。

例如,您可以决定在自上次保存修订以来经过一定次数的自动保存回调后保存当前修订(未保存的更改)。

// Create a new plugin that will handle the autosave logic.
class RevisionHistoryAutosaveIntegration extends Plugin {
    init() {
        this._saveAfter = 100; // Create a new revision after 100 saves.
        this._autosaveCount = 1; // Current autosave counter.
        this._lastCreatedAt = null; // Revision `createdAt` value, when the revision was last autosaved.
    }

    async autosave() {
        const revisionTracker = this.editor.plugins.get( 'RevisionTracker' );
        const currentRevision = revisionTracker.currentRevision;

        if ( currentRevision.createdAt > this._lastCreatedAt ) {
            // Revision was saved or updated in the meantime by a different source (not autosave).
            // Reset the counter.
            this._autosaveCount = 1;
        }

        if ( this._autosaveCount === this._saveAfter ) {
            // We reached the count. Save all changes as a new revision. Reset the counter.
            await revisionTracker.saveRevision();

            this._autosaveCount = 1;
            this._lastCreatedAt = currentRevision.createdAt;
        } else {
            // Try updating the "current revision" with the new document changes.
            // If there are any new changes, the `createdAt` property will change its value.
            // Do not raise the counter, if the revision has not been updated!
            await revisionTracker.update();

            if ( currentRevision.createdAt > this._lastCreatedAt ) {
                this._autosaveCount++;
                this._lastCreatedAt = currentRevision.createdAt;
            }
        }

        return true;
    }
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        extraPlugins: [
            // ...
            // Add the new plugin to the editor configuration:
            RevisionHistoryAutosaveIntegration
        ],
        // ...
        // Add the autosave configuration -- call the plugin method:
        autosave: {
            save: editor => {
                return editor.plugins.get( RevisionHistoryAutosaveIntegration ).autosave();
            }
        }
    } )
    .catch( error => console.error( error ) );

类似地,您可以实现一种保存策略,该策略会包含自上次保存修订以来经过的时间、操作次数或多个变量的组合,以决定是否应该保存新的修订。

# 修订如何更新和保存

了解修订如何以及何时更新和保存对于编写与修订历史记录(包括自动保存集成)集成的自定义代码至关重要。

文档始终至少有两个可用的修订:初始修订和当前修订。如果文档是新的并且还没有为其创建任何修订,则在初始化编辑器时会创建这两个修订。

初始修订包含文档首次初始化时的编辑器数据。它可以为空或包含一些内容。初始修订的 ID 将等于文档 ID 或 'initial'(如果未指定文档 ID)。

当前修订是存储所有未保存的文档更改的修订,即尚未保存在早期修订中的更改。它始终位于修订列表的顶部。如果创建新的修订,它将包含所有未保存的更改,并将添加到当前修订的下方。然后,当前修订将为空,直到再次用新的未保存的文档更改更新。空的当前修订不会显示在修订列表中。

文档更改时,当前修订不会自动更新。可以使用修订功能 API 完成更新。当您需要保存当前修订时,例如在自动保存回调中,更新就足够了。当打开修订历史视图时,也会触发更新。在这种情况下,要么调用自动保存回调,要么更新当前修订(如果未使用自动保存插件)。

可以使用修订功能 API 保存新的修订。此外,在以下情况下会创建新的修订:

  • 用户使用编辑器工具栏中的下拉菜单保存修订。
  • 用户在修订历史视图中为当前修订命名。
  • 每次初始化编辑器时(将创建新的当前修订,而旧的当前修订将成为常规修订)。

# 使用功能 API 保存或更新修订

const revisionTrackerPlugin = this.editor.plugins.get( 'RevisionTracker' );

// Updates the "current revision", that is, the revision containing unsaved changes.
revisionTrackerPlugin.update();

// Creates a new revision that will contain all the unsaved changes.
// See the API reference to learn more.
revisionTrackerPlugin.saveRevision();
revisionTrackerPlugin.saveRevision( { name: 'My revision' } );

# 修订历史记录示例

请访问 ckeditor5-collaboration-samples GitHub 存储库,以查找修订历史记录功能的几个示例集成。