将评论与您的应用程序集成
评论功能提供了一个 API,可让您在编辑器中添加、删除和更新评论。要将所有这些更改保存到您的数据库并访问它们,您首先需要集成此功能。
本指南介绍了将评论作为独立插件(其异步版本)集成。如果您使用的是实时协作,请参阅实时协作功能集成指南。
# 集成方法
本指南将讨论将 CKEditor 5 与您的评论数据源集成的两种方法
- 简单的“加载和保存”集成,直接使用
CommentsRepository
插件 API。 - 适配器集成,当评论数据在编辑器中发生变化时,它会立即更新数据库中的评论数据。
适配器集成是推荐的方法,因为它使您能够更好地控制数据。
# 开始之前
作为本指南的补充,我们提供了可供下载的现成示例。您可以将示例用作示例或集成起点。
# 准备自定义编辑器设置
要使用评论插件,您需要准备一个自定义编辑器设置,其中包含评论功能的异步版本。
最简单的方法是使用Builder。选择一个预设并开始自定义您的编辑器。
Builder 允许您选择首选的分发方法和框架。对于本指南,我们将使用“Vanilla JS”选项,使用“npm”和一个基于“Classic Editor (basic)”预设的简单设置,并启用评论功能。
在 Builder 的“功能”部分(第 2 步)中,请确保
- 关闭“协作”组旁边的“实时”切换按钮。
- 启用“协作→评论”功能。
完成设置后,Builder 将为您提供必要的 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.html
、style.css
和 main.js
。
现在是使用我们自定义编辑器设置的时候了。转到 Builder 的“安装”部分,并将生成的代码片段复制到这 3 个文件中。
# 激活功能
要使用此高级功能,您需要使用许可证密钥激活它。有关详细信息,请参阅 许可证密钥和激活 指南。
成功获取许可证密钥后,打开 main.js
文件,并将 your-license-key
字符串更新为您的许可证密钥。
# 构建项目
最后,通过运行以下命令构建项目:
npm run dev
在浏览器中打开示例时,您应该会看到带有评论插件的所见即所得编辑器。但是,它仍然无法加载或保存任何数据。您将在本指南的后面学习如何将数据添加到评论插件。
现在让我们更深入地了解此设置的结构。
# 基本设置的结构
以下示例为评论线程实施了 宽侧边栏显示模式。如果您想使用内联显示模式,请删除设置侧边栏的片段。
现在让我们浏览此基本设置的关键片段。
# HTML 结构
页面的 HTML 和 CSS 结构创建了两列
<div class="editor-container__editor">
是编辑器使用的容器。<div class="editor-container__sidebar">
是包含批注(即评论)的侧边栏使用的容器。
# JavaScript
main.js
文件设置了编辑器实例
- 加载所有必要的编辑器插件(包括
Comments
插件)。 - 设置
licenseKey
配置选项。 - 将
sidebar.container
配置选项设置为上面提到的容器。 - 将
comment
和commentsArchive
按钮添加到编辑器工具栏。 - 定义我们将在此教程的后续步骤中使用的
CommentsIntegration
和UsersIntegrations
插件的模板。
如果您使用 ImageToolbar
插件,请将 comment
按钮添加到 图像工具栏。
# 评论 API
以下集成使用评论 API。熟悉 API 可能有助于您理解代码片段。如有任何问题,请参阅 评论 API 文档。
# 下一步
我们已经设置了一个简单的 JavaScript 项目,它运行一个带有评论功能异步版本的 CKEditor 5 基本实例。但是,它还没有处理加载或保存数据。接下来的两节将介绍两种可用的集成方法。
# 简单的“加载和保存”集成
在此解决方案中,用户和评论数据在编辑器初始化期间加载,评论数据在您完成使用编辑器后保存(例如,当您提交包含所见即所得编辑器的表单时)。
如果您信任用户或提供提交数据的额外验证,建议使用此方法。这样,我们可以确保用户只更改他们的评论。
作为本指南的补充,我们提供了 可供下载的示例。您可以使用这些示例作为示例或作为您自己的集成的起点。
# 加载数据
当评论插件已包含在编辑器中时,您需要创建一个插件来初始化用户和现有评论。
首先,将用户和评论数据转储到一个变量中,该变量将可供您的插件使用。
如果您的应用程序需要异步从服务器请求评论数据,而不是将数据放在 HTML 源代码中,您可以创建一个插件从数据库获取数据。在这种情况下,您的插件应该 从 Plugin.init()
方法返回一个 Promise
,以确保编辑器初始化等待您的数据。
如果您已按照 “入门”部分中的建议 设置示例项目,请打开 main.js
文件,并在导入语句之后添加此变量
// Application data will be available under a global variable `appData`.
const appData = {
// Users data.
users: [
{
id: 'user-1',
name: 'Mex Haddox'
},
{
id: 'user-2',
name: 'Zee Croce'
}
],
// The ID of the current user.
userId: 'user-1',
// Comment threads data.
commentThreads: [
{
threadId: 'thread-1',
comments: [
{
commentId: 'comment-1',
authorId: 'user-1',
content: '<p>Are we sure we want to use a made-up disorder name?</p>',
createdAt: new Date( '09/20/2018 14:21:53' ),
attributes: {}
},
{
commentId: 'comment-2',
authorId: 'user-2',
content: '<p>Why not?</p>',
createdAt: new Date( '09/21/2018 08:17:01' ),
attributes: {}
}
],
context: {
type: 'text',
value: 'Bilingual Personality Disorder'
},
unlinkedAt: null,
resolvedAt: null,
resolvedBy: null,
attributes: {}
}
],
// Editor initial data.
initialData:
`<h2>
<comment-start name="thread-1"></comment-start>
Bilingual Personality Disorder
<comment-end name="thread-1"></comment-end>
</h2>
<p>
This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
As recent studies show, the language you speak has more effects on you than you realize.
According to the studies, the language a person speaks affects their cognition,
behavior, emotions and hence <strong>their personality</strong>.
</p>
<p>
This shouldn’t come as a surprise
<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
that different regions of the brain become more active depending on the activity.
The structure, information and especially <strong>the culture</strong> of languages varies substantially
and the language a person speaks is an essential element of daily life.
</p>`
};
Builder 的输出示例已经提供了两个插件的模板:UsersIntegration
和 CommentsIntegration
。用读取 appData
中的数据并使用 Users
和 CommentsRepository
API 的模板替换它们
class UsersIntegration extends Plugin {
static get requires() {
return [ 'Users' ];
}
static get pluginName() {
return 'UsersIntegration';
}
init() {
const usersPlugin = this.editor.plugins.get( 'Users' );
// Load the users data.
for ( const user of appData.users ) {
usersPlugin.addUser( user );
}
// Set the current user.
usersPlugin.defineMe( appData.userId );
}
}
class CommentsIntegration extends Plugin {
static get requires() {
return [ 'CommentsRepository', 'UsersIntegration' ];
}
static get pluginName() {
return 'CommentsIntegration';
}
init() {
const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );
// Load the comment threads data.
for ( const commentThread of appData.commentThreads ) {
commentsRepositoryPlugin.addCommentThread( commentThread );
}
}
}
更新 editorConfig.initialData
属性以使用 appData.initialData
值
const editorConfig = {
// ...
initialData: appData.initialData
// ...
};
并构建项目
npm run dev
您现在应该会看到一个带有评论线程的编辑器实例。
# 保存数据
要保存评论数据,您首先需要使用 CommentsRepository
API 获取它。为此,请使用 getCommentThreads()
方法。
然后,使用评论线程数据以您喜欢的方式将其保存到您的数据库中。请参阅以下示例。
在 index.html
中添加
<button id="get-data">Get data</button>
在 main.js
中使用链式 then()
更新 ClassicEditor.create()
调用
ClassicEditor
.create( /* ... */ )
.then( editor => {
// After the editor is initialized, add an action to be performed after a button is clicked.
const commentsRepository = editor.plugins.get( 'CommentsRepository' );
// Get the data on demand.
document.querySelector( '#get-data' ).addEventListener( 'click', () => {
const editorData = editor.data.get();
const commentThreadsData = commentsRepository.getCommentThreads( {
skipNotAttached: true,
skipEmpty: true,
toJSON: true
} );
// Now, use `editorData` and `commentThreadsData` to save the data in your application.
// For example, you can set them as values of hidden input fields.
console.log( editorData );
console.log( commentThreadsData );
} );
} );
建议将 attributes
值字符串化为 JSON,将其作为字符串保存在数据库中,然后在加载评论时从 JSON 解析该值。
# 演示
控制台
// Use the `Save data with comments` button to see the result...
# 适配器集成
适配器集成使用您提供的适配器对象,以立即将评论中的更改保存到您的数据存储中。这是将评论与应用程序集成的推荐方法,因为它让您可以更安全地处理客户端-服务器通信。例如,您可以检查用户权限、验证发送的数据或使用在服务器端获取的信息(如评论创建时间)更新数据。您将在以下步骤中看到如何处理服务器响应。
作为本指南的补充,我们提供了 可供下载的示例。您可以使用这些示例作为示例或作为您自己的集成的起点。
# 实施
首先,使用 CommentsRepository#adapter
属性定义适配器。 适配器方法 在用户对评论进行更改后被调用。适配器允许您立即将更改保存到您的数据库中。每个评论操作都有一个单独的适配器方法,您应该实施这些方法。
在 UI 端,对评论的每个更改都会立即执行,但是,所有适配器操作都是异步的,并在后台执行。因此,所有适配器方法都需要返回一个 Promise
。当 promise 解析时,表示一切正常,本地更改已成功保存在数据存储中。当 promise 被拒绝时,编辑器会抛出一个 CKEditorError
错误,它与 看门狗 功能配合得很好。在处理服务器响应时,您可以决定是否应该解析或拒绝 promise。
在执行任何适配器操作时,一个挂起操作会自动添加到编辑器 PendingActions
插件中,因此您不必担心编辑器会在适配器操作完成之前被销毁。
现在您可以实施适配器了。
如果您已按照 “入门”部分中的建议 设置示例项目,请打开 main.js
文件,并在导入语句之后添加此变量
// Application data will be available under a global variable `appData`.
const appData = {
// Users data.
users: [
{
id: 'user-1',
name: 'Mex Haddox'
},
{
id: 'user-2',
name: 'Zee Croce'
}
],
// The ID of the current user.
userId: 'user-1',
// Editor initial data.
initialData:
`<h2>
<comment-start name="thread-1"></comment-start>
Bilingual Personality Disorder
<comment-end name="thread-1"></comment-end>
</h2>
<p>
This may be the first time you hear about this made-up disorder but it actually isn’t so far from the truth.
As recent studies show, the language you speak has more effects on you than you realize.
According to the studies, the language a person speaks affects their cognition,
behavior, emotions and hence <strong>their personality</strong>.
</p>
<p>
This shouldn’t come as a surprise
<a href="https://en.wikipedia.org/wiki/Lateralization_of_brain_function">since we already know</a>
that different regions of the brain become more active depending on the activity.
The structure, information and especially <strong>the culture</strong> of languages varies substantially
and the language a person speaks is an essential element of daily life.
</p>`
};
Builder 的输出示例已经提供了两个插件 UsersIntegration
和 CommentsIntegration
的模板。用读取 appData
中的数据并使用 Users
和 CommentsRepository
API 的模板替换它们
class UsersIntegration extends Plugin {
static get requires() {
return ['Users'];
}
static get pluginName() {
return 'UsersIntegration';
}
init() {
const usersPlugin = this.editor.plugins.get( 'Users' );
// Load the users data.
for ( const user of appData.users ) {
usersPlugin.addUser( user );
}
// Set the current user.
usersPlugin.defineMe( appData.userId );
}
}
class CommentsIntegration extends Plugin {
static get requires() {
return [ 'CommentsRepository', 'UsersIntegration' ];
}
static get pluginName() {
return 'CommentsIntegration';
}
init() {
const commentsRepositoryPlugin = this.editor.plugins.get( 'CommentsRepository' );
// Set the adapter on the `CommentsRepository#adapter` property.
commentsRepositoryPlugin.adapter = {
addComment( data ) {
console.log( 'Comment added', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
// When the promise resolves with the comment data object, it
// will update the editor comment using the provided data.
return Promise.resolve( {
createdAt: new Date() // Should be set on the server side.
} );
},
updateComment( data ) {
console.log( 'Comment updated', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve();
},
removeComment( data ) {
console.log( 'Comment removed', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve();
},
addCommentThread( data ) {
console.log( 'Comment thread added', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve( {
threadId: data.threadId,
comments: data.comments.map( ( comment ) => ( { commentId: comment.commentId, createdAt: new Date() } ) ) // Should be set on the server side.
} );
},
getCommentThread( data ) {
console.log( 'Getting comment thread', data );
// Write a request to your database here. The returned `Promise`
// should resolve with the comment thread data.
return Promise.resolve( {
threadId: data.threadId,
comments: [
{
commentId: 'comment-1',
authorId: 'user-2',
content: '<p>Are we sure we want to use a made-up disorder name?</p>',
createdAt: new Date(),
attributes: {}
}
],
// It defines the value on which the comment has been created initially.
// If it is empty it will be set based on the comment marker.
context: {
type: 'text',
value: 'Bilingual Personality Disorder'
},
unlinkedAt: null,
resolvedAt: null,
resolvedBy: null,
attributes: {},
isFromAdapter: true
} );
},
updateCommentThread( data ) {
console.log( 'Comment thread updated', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve();
},
resolveCommentThread( data ) {
console.log( 'Comment thread resolved', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve( {
resolvedAt: new Date(), // Should be set on the server side.
resolvedBy: usersPlugin.me.id // Should be set on the server side.
} );
},
reopenCommentThread( data ) {
console.log( 'Comment thread reopened', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve();
},
removeCommentThread( data ) {
console.log( 'Comment thread removed', data );
// Write a request to your database here. The returned `Promise`
// should be resolved when the request has finished.
return Promise.resolve();
}
};
}
}
更新 editorConfig.initialData
属性以使用 appData.initialData
值
const editorConfig = {
// ...
initialData: appData.initialData
// ...
};
并构建项目
npm run dev
您现在应该会看到一个带有评论线程的编辑器实例。在您与编辑器中的评论功能进行交互时(添加/删除线程和评论),请观察浏览器控制台。
建议将 attributes
值字符串化为 JSON,将其作为字符串保存在数据库中,然后在加载评论时从 JSON 解析该值。
# 演示
挂起适配器操作控制台
// Add, remove or update a comment to see the result...
由于评论适配器在评论执行更改后立即保存更改,因此建议使用 Autosave 插件在每次更改后保存编辑器内容。
# 为什么当我从内容中删除评论线程标记时没有事件?
请注意,当您删除与评论线程相对应的标记时,不会触发任何删除事件。相反,评论线程将被解决,从而触发 CommentsRepository#resolveCommentThread
事件。此操作可以使用撤消(Cmd+Z 或 Ctrl+Z)恢复,这将触发 CommentsRepository#reopenCommentThread
事件。
但是,您仍然可以使用批注中的可用按钮删除评论线程。请记住,删除操作无法撤消。
在实施适配器方法(getCommentThread()
和 getComment()
)时,请确保它们不返回已删除的数据,因为删除被视为永久操作。
# 评论示例
请访问 ckeditor5-collaboration-samples
GitHub 存储库,以查找评论功能的几个示例集成。
我们每天都在努力使我们的文档保持完整。您是否发现了过时的信息?缺少什么东西?请通过我们的 问题跟踪器 报告。
随着 42.0.0 版本的发布,我们重新编写了大部分文档以反映新的导入路径和功能。我们感谢您的反馈,以帮助我们确保文档的准确性和完整性。