尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CKEditor 5 Watchdog 崩溃恢复指南:用 EditorWatchdog 与 ContextWatchdog 守护编辑器实例

CKEditor 5 Watchdog 崩溃恢复指南:用 EditorWatchdog 与 ContextWatchdog 守护编辑器实例 CKEditor 5 Watchdog 崩溃恢复指南用 EditorWatchdog 与 ContextWatchdog 守护编辑器实例【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5Watchdog 是 CKEditor 5 中用于守护编辑器运行状态的工具组件它能在编辑器崩溃前保存内容快照、销毁异常实例并自动重建从而最大程度避免数据丢失。本文以当前仓库中的 Watchdog 功能文档为主线结合ckeditor/ckeditor5-watchdog包的源码实现完整讲解 EditorWatchdog 与 ContextWatchdog 两种类型的用法、事件与状态机、配置参数以及使用边界帮助你为生产环境中的富文本编辑器构建可靠的自动恢复机制。Watchdog 解决了什么问题任何非平凡的软件都有缺陷。即便 CKEditor 5 在发布前经过了 100% 代码覆盖率、回归测试和人工测试编辑器本身、用户所用的浏览器、集成了编辑器的应用代码以及你使用的第三方插件依然都可能抛出异常。为了把一次编辑器崩溃对用户体验的影响降到最低Watchdog 的思路是自动重启编辑器并让它带上崩溃前保存的内容。Watchdog 会持续盯住一个编辑器实例一旦检测到它崩溃就销毁旧实例并按照崩溃前保存的数据创建一个全新的实例。在 CKEditor 5 API 中最危险的位置比如editor.model.change()、editor.editing.view.change()以及各类 emitter 内部都已经内置了错误检测与try-catch处理Watchdog 正是在这些环节捕获未知错误并触发重启。仓库提供两种 Watchdog 类型选择依据是你的应用是否使用了 ContextEditor watchdog——用于守护单个编辑器实例Context watchdog——用于应用使用了 Context多个编辑器共享上下文的场景。两者都实现在ckeditor/ckeditor5-watchdog包中公共导出见 packages/ckeditor5-watchdog/src/index.ts该包是ckeditor5完整发行包的一部分直接npm install ckeditor5即可使用。使用前提Watchdog 需要接管创建过程Watchdog 有一个重要前提——它必须能访问编辑器的创建过程。具体来说只能在编辑器实例以编程方式创建的场景下使用例如通过Editor.create()或自定义 creator 回调预构建编辑器prebuilt builds和框架集成只要提供了 creator 函数或集成暴露了 watchdog 配置也受支持如果编辑器是声明式初始化declarative或者通过全局脚本初始化、创建过程不受应用控制则无法使用 Watchdog。从源码看这一前提对应 editorwatchdog.ts 中的构造函数传入编辑器类后包会为其生成默认 creatorEditor.create( ... )与默认 destructoreditor.destroy()后续所有重建都由这条 creator 回调驱动。因此请确保编辑器实例由你的代码创建而不是散落在页面上的classckeditor等自动初始化逻辑中。Editor watchdog守护单个编辑器安装编辑器之后参见快速开始指南最简单的接入方式是把ClassicEditor.create()替换为watchdog.create()import { ClassicEditor, Bold, EditorWatchdog, Essentials, Italic, Paragraph } from ckeditor5; // Create a watchdog for the given editor type. const watchdog new EditorWatchdog( ClassicEditor ); // Create a new editor instance. watchdog.create( { attachTo: document.querySelector( #editor ), licenseKey: YOUR_LICENSE_KEY, // Or GPL. plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ bold, italic, alignment ] } );这里watchdog.create()的入参就是原本传给ClassicEditor.create()的配置对象。代码整体分为两步先基于编辑器类构造 watchdog再由 watchdog 去创建编辑器实例。此后 watchdog 会持续监视该实例一旦崩溃就自动重建一个新编辑器。重要约定不要把编辑器实例保存在应用自己的状态里。每次崩溃后 watchdog 都会创建一个全新实例旧实例已经失效。要访问当前实例应始终使用watchdog.editor属性任何需要在新实例上执行的逻辑应作为编辑器插件加载或在setCreator()/setDestructor()回调中执行详见下一节。从源码结构看editor是一个 getter见 editorwatchdog.ts内部维护在_editor字段上重启过程中该字段会被替换为新实例。控制编辑器的创建与销毁默认行为之外你可以通过EditorWatchdog#setCreator()和EditorWatchdog#setDestructor()获得对创建、销毁过程的完全控制// Create an editor watchdog. const watchdog new EditorWatchdog(); // Define a callback that will create an editor instance and return it. watchdog.setCreator( ( editorConfig ) { return ClassicEditor .create( editorConfig ) .then( editor { // Do something with the new editor instance. // ... } ); } ); // Do something before the editor is destroyed. Return a promise. watchdog.setDestructor( editor { // Do something before the editor is destroyed. // ... return editor .destroy() .then( () { // Do something after the editor is destroyed. // ... } ); } ); // Create an editor instance and start watching it. watchdog.create( editorConfig );注意未通过setDestructor()覆盖时默认的编辑器销毁逻辑就是执行Editor#destroy()在构造函数中由this._destructor editor editor.destroy()建立。而在 editorwatchdog.ts 的_destroy()实现中可以看到销毁前 watchdog 还会先移除change:data监听器——这是为了避免销毁阶段插件再触发数据变更事件、导致在编辑器已部分销毁时调用getData()而引发二次错误。从源码还可以看到create()支持两种调用形态Watchdog 会自动检测config-based 模式第一参数是配置对象例如watchdog.create( config )legacy 模式第一参数是源元素/数据第二参数是配置例如watchdog.create( element, config )该签名已标记为 deprecated未来版本会移除。内部通过_detectConfigBasedCreator()判断字符串或 DOM 元素、带非空对象第二参数、以及所有值都是字符串/元素的对象都被归为 legacy 模式其余情况视为 config-based 模式。若你正在编写新代码建议统一使用 config-based 写法。Editor watchdog 的 API事件、状态与崩溃记录Editor watchdog 提供的方法、属性和事件总结如下watchdog.on( error, () { console.log( Editor crashed. ) } ); watchdog.on( restart, () { console.log( Editor was restarted. ) } ); // Destroy the watchdog and the current editor instance. watchdog.destroy(); // The current editor instance. watchdog.editor; // The current state of the editor. // The editor might be in one of the following states: // // * initializing - Before the first initialization, and after crashes, before the editor is ready. // * ready - A state when the user can interact with the editor. // * crashed - A state when an error occurs. It quickly changes to initializing or crashedPermanently depending on how many and how frequent errors have been caught recently. // * crashedPermanently - A state when the watchdog stops reacting to errors and keeps the editor crashed. // * destroyed - A state when the editor is manually destroyed by the user after calling watchdog.destroy(). watchdog.state; // Listen to state changes. let prevState watchdog.state; watchdog.on( stateChange, () { const currentState watchdog.state; console.log( State changed from ${ currentState } to ${ prevState } ); if ( currentState crashedPermanently ) { watchdog.editor.enableReadOnlyMode( crashed-editor ); } prevState currentState; } ); // An array of editor crash information. watchdog.crashes.forEach( crashInfo console.log( crashInfo ) );上述state的五种取值、crashes数组、error/restart/stateChange事件都在抽象基类 watchdog.ts 中定义并实现。这里有几个值得注意的源码细节错误捕获渠道_startErrorHandling()会在window上同时挂载error与unhandledrejection两个监听器因此无论异常是同步抛出的还是出现在未捕获的 Promise rejection 中都能被 Watchdog 感知见 watchdog.ts。只处理 CKEditorError_shouldReactToError()要求错误是CKEditorError、带有非undefined且非null的context、且当前状态为ready并通过areConnectedThroughProperties见 packages/ckeditor5-watchdog/src/utils/areconnectedthroughproperties.ts确认错误确实来自被监视的编辑器。context null的语义是初始化阶段发生的错误不触发重启因为这类错误属于集成代码问题详见文末限制一节。崩溃信息结构每次捕获到相关错误crashes数组都会追加{ message, stack, filename, lineno, colno, date }其中filename/lineno/colno仅在ErrorEvent上可用。崩溃后如何恢复内容Watchdog 之所以能在崩溃后恢复数据是因为它在编辑器正常运行时就在持续保存快照。核心链路如下见 editorwatchdog.ts编辑器创建成功后editor.model.document.on( change:data, this._throttledSave )订阅数据变更事件_throttledSave是一个按saveInterval节流的保存函数_save()/_getData()会序列化当前模型内容——包括各 root 的内容与属性、数据相关 markers以及可选的评论线程CommentsRepository与修订建议TrackChanges数据重启时_restart()会重新规范化 root 配置见 packages/ckeditor5-watchdog/src/utils/normalizerootsconfig.ts并把快照数据以_watchdogInitialData注入新配置同时追加一个内部插件EditorWatchdogInitPluginEditorWatchdogInitPlugin在editor.data的init事件中停止默认初始化流程用保存的快照重建模型节点、root 属性与 markers并恢复协作数据评论与修订建议最后触发ready。这意味着对于使用了评论Comments或修订Track Changes等协作功能的编辑器崩溃重启后这些协作数据也能一并恢复。不过在继续之前先说明一个通用约束由于每次崩溃都创建全新实例任何需要在新实例上执行的逻辑都应作为编辑器插件或 creator/destructor 回调来实现切勿持有旧实例引用。Context watchdog守护共享上下文当应用使用 Context多个编辑器共享同一上下文例如共享协作会话时单个的 EditorWatchdog 不够用需要ContextWatchdog。它同时守护上下文本身以及挂载在其上的多个item目前仅支持type: editor的编辑器。安装编辑器后按如下方式创建 Context watchdog 并注册编辑器import { ClassicEditor, ContextWatchdog, Bold, Italic, Context, Essentials, Paragraph } from ckeditor5; // Create a context watchdog and pass the context class with optional watchdog configuration: const watchdog new ContextWatchdog( Context, { crashNumberLimit: 10 } ); // Initialize the watchdog with the context configuration: await watchdog.create( { plugins: [ // A list of plugins for the context. // ... ], // More configuration options for the plugin. // ... } ); // Add editor instances. // You may also use multiple ContextWatchdog#add() calls, each adding a single editor. await watchdog.add( [ { id: editor1, type: editor, config: { attachTo: document.querySelector( #editor ), plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ bold, italic, alignment ] }, creator: ( config ) ClassicEditor.create( config ) }, { id: editor2, type: editor, config: { attachTo: document.querySelector( #editor-2 ), plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ bold, italic, alignment ] }, creator: ( config ) ClassicEditor.create( config ) } ] ); // Or: await watchdog.add( { id: editor1, type: editor, config: { attachTo: document.querySelector( #editor ), plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ bold, italic, alignment ] }, creator: ( config ) ClassicEditor.create( config ) } ); await watchdog.add( { id: editor2, type: editor, config: { attachTo: document.querySelector( #editor-2 ), plugins: [ Essentials, Paragraph, Bold, Italic ], toolbar: [ bold, italic, alignment ] }, creator: ( config ) ClassicEditor.create( config ) } );每个 item 配置的核心字段为id唯一标识、type目前只支持editor、config编辑器配置、creator创建函数接收配置并返回 Promise。add()可以一次传入数组批量注册也可以多次逐个调用——源码注释建议为性能考虑尽量一次性批量传入见 contextwatchdog.ts。需要销毁某个 item 时使用ContextWatchdog#removeawait watchdog.remove( [ editor1, editor2 ] ); // Or: await watchdog.remove( editor1 ); await watchdog.remove( editor2 );从 contextwatchdog.ts 的实现看ContextWatchdog内部为每个 item 创建独立的EditorWatchdog构造时传入null编辑器类再用 item 的creator通过setCreator()接管并借助ActionQueues保证操作串行主队列context 的 create/destroy/restart会等待所有 item 队列item 操作只等待主队列与自己的队列从而避免并发竞态。Context watchdog 的 APIContext watchdog 提供的完整 API 如下// Creating a watchdog that will use the context class and the watchdog configuration. const watchdog new ContextWatchdog( Context, watchdogConfig ); // Setting a custom creator for the context. watchdog.setCreator( async config { const context await Context.create( config ); // Do something when the context is initialized. // ... return context; } ); // Setting a custom destructor for the context. watchdog.setDestructor( async context { // Do something before destroy. // ... await context.destroy(); } ); // Initializing the context watchdog with the context configuration. await watchdog.create( contextConfig ); // Adding item configuration (or an array of item configurations). await watchdog.add( { id: editor1, type: editor, config: editorConfig, creator: createEditor, destructor: destroyEditor, } ); await watchdog.add( [ { id: editor1, type: editor, config: editorConfig, creator: createEditor, destructor: destroyEditor, }, // More configuration items. // ... ] ); // Remove and destroy a given item (or items). await watchdog.remove( editor1 ); await watchdog.remove( [ editor1, editor2, ... ] ); // Getting the given item instance. const editor1 watchdog.getItem( editor1 ); // Getting the state of the given item. const editor1State watchdog.getItemState( editor1 ); // Getting the context state. const contextState watchdog.state; // The error event is fired when the context watchdog catches a context-related error. // Note that errors fired by items are not delegated to ContextWatchdog#event:error. // See also ContextWatchdog#event:itemError. watchdog.on( error, ( _, { error } ) { // The restart event is fired when the context is set back to the ready state (after it was in the crashed state). // Similarly, this event is not thrown for internal item restarts. watchdog.on( restart, () { console.log( The context has been restarted. ); } ); // The itemError event is fired when an error occurred in one of the added items. watchdog.on( itemError, ( _, { error, itemId } ) { console.log( An error occurred in an item with the ${ itemId } ID. ); } ); // The itemRestart event is fired when an item is set back to the ready state (after it was in the crashed state). watchdog.on( itemRestart, ( _, { itemId } ) { console.log( An item with with the ${ itemId } ID has been restarted. ); } );需要特别区分两组事件error与restart只针对上下文本身item 抛出的错误不会委托给ContextWatchdog#event:erroritem 的错误与重启对应itemError与itemRestart事件载荷中带有itemId用于定位是哪个编辑器出问题。从源码看_isErrorComingFromThisItem()会先遍历所有内部 EditorWatchdog如果错误来自某个 item则 context 不处理返回false由对应的 EditorWatchdog 走自己的错误流程只有错误确实关联到 context 本身时才触发 context 重启。这正是上下文错误重启上下文、item 错误只重启 item这一隔离机制的实现基础。配置参数EditorWatchdog与ContextWatchdog构造函数都接受第二个参数——一个配置对象WatchdogConfig类型定义见 watchdog.ts包含以下可选属性配置项说明默认值crashNumberLimit崩溃次数阈值。达到该次数且最近几次错误之间的平均间隔小于minimumNonErrorTimePeriod时watchdog 进入crashedPermanently状态并停止重启防止无限重启死循环3minimumNonErrorTimePeriod最近几次编辑器错误之间的平均毫秒数阈值。当平均间隔低于该值且同时达到crashNumberLimit时进入crashedPermanently并停止重启同样用于防止无限重启循环5000saveInterval内部保存编辑器数据的两次操作之间的最小毫秒间隔。注意对于大文档频繁保存可能影响编辑器性能5000示例const editorWatchdog new EditorWatchdog( ClassicEditor, { minimumNonErrorTimePeriod: 2000, crashNumberLimit: 4, saveInterval: 1000 } );这两个防止无限重启的阈值在 watchdog.ts 的_shouldRestart()中组合生效当crashes数量未超过crashNumberLimit时总是重启超过后则计算最近若干次错误的平均无错间隔只有该间隔大于minimumNonErrorTimePeriod才继续重启否则转入crashedPermanently。也就是说偶尔一次崩溃会立即恢复而短时间内的连续崩溃则会让 watchdog 停止徒劳的重启把问题留给上层处理。另外有一点需要记住Context watchdog 会把它收到的配置透传给它为每个 item 创建的 EditorWatchdog。所以给ContextWatchdog设置saveInterval等参数其管理的所有编辑器都会采用相同配置。限制与边界Watchdog 不是万能的使用前请明确以下边界Watchdog 不处理初始化与销毁阶段的错误。例如Editor.create()或Context.create()期间抛出的错误以及Editor#destroy()/Context#destroy()期间的错误都不会被捕获和重启。原因在于这些阶段报错说明集成代码本身存在问题比如配置非法、DOM 元素不存在这类问题无法通过重启编辑器解决。这也是 watchdog.ts 中_shouldReactToError()要求错误context非null的原因——初始化阶段错误以null上下文标记Watchdog 会主动忽略。另外两类配套限制也需要纳入架构设计不要在应用状态中缓存编辑器实例始终通过watchdog.editor/watchdog.getItem( id )访问崩溃属于异常路径请结合crashedPermanently状态与stateChange事件为最终无法恢复的场景准备降级方案——例如上述示例中在crashedPermanently时调用editor.enableReadOnlyMode()将编辑器切换为只读避免用户在不可靠状态下继续编辑。手动验证 Watchdog 行为仓库在 packages/ckeditor5-watchdog/manual 提供了多个可运行的手动测试页面如watchdog.manual.html、watchdog-data.manual.html、watchdog-multi-root-elements.manual.html等对应源码 watchdog.ts 展示了完整的验证套路注册一个TypingError插件在用户输入特定字符如1时故意访问不存在的属性this.editor.foo.bar bom制造崩溃随后观察 watchdog 的error、restart、stateChange事件与页面上的状态展示当状态进入crashedPermanently时调用enableReadOnlyMode()保护编辑器。手动测试覆盖了普通编辑器、data 模式、balloon 编辑器以及 multi-root 编辑器等不同形态是复现与调试 Watchdog 行为最直接的途径。小结Watchdog 为 CKEditor 5 提供了开箱即用的崩溃自愈能力EditorWatchdog 守护单实例ContextWatchdog 守护共享上下文及其下的多个编辑器两者通过全局错误监听识别来自被监视对象的 CKEditorError依据crashNumberLimit与minimumNonErrorTimePeriod决定立即重启还是进入crashedPermanently并在后台按saveInterval节流保存内容快照含模型内容、markers 及协作数据确保崩溃后能以崩溃前的内容重建编辑器。接入时只需记住把创建过程交给 watchdog、通过editor/getItem()访问当前实例、在stateChange中为永久崩溃准备降级策略就能显著提升富文本编辑功能的稳定性。【免费下载链接】ckeditor5Powerful rich text editor framework with a modular architecture, modern integrations, and features like collaborative editing.项目地址: https://gitcode.com/GitHub_Trending/ck/ckeditor5创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表