Преглед на файлове

fix: prevent revoked editor state proxies

Developer преди 4 седмици
родител
ревизия
75364fa2d5
променени са 3 файла, в които са добавени 38 реда и са изтрити 36 реда
  1. BIN
      dist.zip
  2. 20 36
      src/components/editor/index.vue
  3. 18 0
      tests/editor-toolbar-selection.test.js

BIN
dist.zip


+ 20 - 36
src/components/editor/index.vue

@@ -19,18 +19,15 @@ export default {
         }
     },
     data() {
-        return {
-            editor: null,
-            toolbar: null
-        }
+        return {}
     },
     watch: {
         content(newVal) {
             // 外部 prop 变化(异步加载详情)才回填,避免输入过程中光标跳动与循环触发
             // setHtml 在内部 Slate state 半销毁状态下会抛 "proxy has been revoked",
             // 用 try-catch 兜住,不让 watcher 异常把整个 Vue 组件标为 error 状态
-            if (this.editor && newVal !== this.editor.getHtml()) {
-                try { this.editor.setHtml(newVal || '') } catch (e) { /* swallow */ }
+            if (this._editor && newVal !== this._editor.getHtml()) {
+                try { this._editor.setHtml(newVal || '') } catch (e) { /* swallow */ }
             }
         }
     },
@@ -48,7 +45,7 @@ export default {
         //   2) 新建的 editor 工具栏被旧 DOM 遮挡,视觉上像"不可点击"
         // mounted 已经完成创建,这里只做幂等占位;内容回填由 watch content → setHtml 处理
         init() {
-            if (!this.editor) {
+            if (!this._editor) {
                 this._createEditor()
             }
         },
@@ -61,13 +58,13 @@ export default {
             // 一点页面就抛 "Cannot perform 'get' on a proxy that has been revoked"。
             // 同步调用 + try-catch 兜住 destroy 链内部任何抛错,保证 emit("destroyed")
             // 至少能走到,监听器能被摘掉。
-            if (this.editor) {
-                try { this.editor.destroy() } catch (e) { /* swallow */ }
-                this.editor = null
+            if (this._editor) {
+                try { this._editor.destroy() } catch (e) { /* swallow */ }
+                this._editor = null
             }
-            if (this.toolbar) {
-                try { this.toolbar.destroy() } catch (e) { /* swallow */ }
-                this.toolbar = null
+            if (this._toolbar) {
+                try { this._toolbar.destroy() } catch (e) { /* swallow */ }
+                this._toolbar = null
             }
         },
         _createEditor() {
@@ -113,31 +110,18 @@ export default {
                 }
             }
 
-            this.editor = createEditor({
+            // 不要把 wangEditor 实例放进 Vue data。Vue 2 会递归观察 Slate
+            // 的 children/selection 等内部对象,生成响应式 Proxy;Slate 在
+            // 下一次操作时会撤销旧 Proxy,随后 selectionchange/editor.focus
+            // 就会报 "Cannot perform 'get' on a proxy that has been revoked"。
+            // 以实例私有字段保存,避免 Vue 对编辑器状态做响应式代理。
+            const editor = createEditor({
                 selector: this.$refs.editorContainer,
                 config: editorConfig,
                 mode: 'default'
             })
 
-            // 兜底:把 editor.apply 包一层 try-catch。Slate.js 在每次 op 后会
-            // revoke 旧 state 代理;如果 selectionchange 触发的 editor.select →
-            // apply 路径撞上一个 stale 代理(常见于 setHtml 跑了一串 op 后,
-            // 监听器 closure 还指向早期 state),会直接 Uncaught。捕获后该次
-            // 选择同步丢弃,不影响后续输入;后续 Slate 自己的 op 流程仍走原 apply,
-            // 这里只是把"已是 normal"的封死,不是替换实现。
-            if (this.editor && typeof this.editor.apply === 'function') {
-                const origApply = this.editor.apply
-                this.editor.apply = function(op) {
-                    try {
-                        return origApply.call(this, op)
-                    } catch (err) {
-                        if (err && err.message && err.message.indexOf('proxy has been revoked') !== -1) {
-                            return null
-                        }
-                        throw err
-                    }
-                }
-            }
+            this._editor = editor
 
             // 工具栏:列出 wangEditor 5 已注册的全部有效菜单 key
             // 注:wangEditor 把删除线菜单命名为 "through","mergeCells" 不是注册菜单
@@ -178,8 +162,8 @@ export default {
                 ]
             }
 
-            this.toolbar = createToolbar({
-                editor: this.editor,
+            this._toolbar = createToolbar({
+                editor,
                 selector: this.$refs.toolbarContainer,
                 config: toolbarConfig,
                 mode: 'default'
@@ -200,6 +184,6 @@ export default {
 }
 .wangeditor-wrapper .editor-container {
     min-height: 600px;
-    height: auto !important;
+    height: 600px !important;
 }
 </style>

+ 18 - 0
tests/editor-toolbar-selection.test.js

@@ -11,3 +11,21 @@ assert.match(
   /onCreated\s*:\s*\(editor\)\s*=>\s*\{\s*editor\.focus\(\)/,
   'editor must initialize a Slate selection after creation so toolbar buttons are enabled'
 )
+
+assert.match(
+  source,
+  /data\(\)\s*\{\s*return\s*\{\}\s*\}/,
+  'wangEditor instances must stay outside Vue 2 reactive data to avoid revoked Slate proxies'
+)
+
+assert.match(
+  source,
+  /const editor\s*=\s*createEditor[\s\S]*this\._editor\s*=\s*editor/,
+  'editor must be stored as a non-reactive private instance field'
+)
+
+assert.match(
+  source,
+  /height:\s*600px\s*!important/,
+  'editor container must be at least 300px high for modal and hoverbar positioning'
+)