600行代码,就为了让 wangEditor5 在 vue3 中更好用

·  阅读 857

1、前言

wangEditor 5 已经公测有一段时间了,在公测群里经常有人问一些官方提供的 vue 组件相关的问题,因此我从使用这角度总结了一下当前 @wangeditor/editor-for-vue@next 存在的一些缺陷:

  • editorId 的设定让我们能拿到编辑器的实例,虽然达到了最终效果,但使用起来并不是怎么高效,也不够傻瓜。并且需要在编辑器销毁后使用 editorId 来手动清除缓存,用户如果忘记了这一步就会造成内存泄漏。
  • 异步设置内容时需要额外的变量来控制编辑器的创建,增加了使用的复杂度,同样不够傻瓜
  • defaultContent 必须是深度克隆的数据,且把这一操作交给了用户,增加使用复杂度
  • 不支持 v-model,把数据的同步推给了用户,增加使用复杂度
  • vue 是一个支持双向绑定的框架,而我们的组件库的一些特殊配置项并不是响应式的,如果用户能通过 editable.config.readOnly = false 就可以禁用编辑器,使用 editable.mode = 'simple' 即可切换编辑器模式,那么将为用户省略 n 多行代码,使用起来更傻瓜
  • 对事件的处理,明明可以直接使用 config.onChange = () => {} 却硬要单独提出来,这一点我不是很理解
  • 特殊场景:在一个页面内,左侧是文章列表,右侧是编辑器,点击列表中的文章,编辑器自动显示文章内容且不会有历史记录(之前 QQ 群中一用户的需求)。针对这种情况,用户只能不断的控制某个变量,给变量先赋值 false 再赋值 true 来销毁重建编辑器,如果能为用户提供一个 reloadEditor API 将使使用更简单

针对上面的缺陷,在 21/12/21 这天我开始尝试自己封装一个自认为好用的 vue3 组件。到 21/12/30 这天算是全面完工。

该组件支持的功能有:

  • 支持动态配置编辑器参数(编辑器创建后修改配置项任生效)
  • 支持 v-modelv-model:html 两种形式的双向绑定
  • 支持动态显示默认内容而不会存在旧文档的历史记录
  • 同时默认内容的配置项支持 json arrayjson stringhtml string 三种格式的数据
  • 天然支持 TypeScript

由于个中缘由,目前不发布 npm 包,如有需要可以 GitHub 自取(仅一个文件),如果觉得好用不妨给个 star

2、自封组件的使用

2.1、全局注册组件

import { createApp } from 'vue'
import wangeditor from 'xxx/wangeditor'

// 全局注册 EditorToolbar, EditorEditable 两个组件
createApp(App).use(wangeditor).mount('#app')
复制代码

2.2、快速开始

<style lang="scss">
  .border {
    border: 1px solid #ddd;
  }
</style>

<template>
  <editor-toolbar class="border" :option="toolbar" @reloadbefore="onToolbarReloadBefore" />
  <editor-editable
    class="border"
    :option="editable"
    v-model="formData.json"
    v-model:html="formData.html"
    @reloadbefore="onEditableReloadBefore"
  />
</template>

<script lang="ts">
  import { Descendant } from 'slate'
  import {
    EditorEditable,
    EditorEditableOption,
    EditorToolbar,
    EditorToolbarOption,
    useWangEditor,
  } from 'xxx/wangeditor'
  import { defineComponent, shallowReactive } from 'vue'

  export default defineComponent({
    components: { EditorToolbar, EditorEditable },
    setup() {
      // 编辑器配置
      const editableOption: EditorEditableOption = {}

      // 菜单栏配置
      const toolbarOption: EditorToolbarOption = {}

      // 防抖时长。当会触发重载的配置项发生变化 365ms 后,编辑器会重载
      const reloadDelary = 365

      const { editable, toolbar, getEditable, getToolbar, clearContent, reloadEditor } = useWangEditor(
        editableOption,
        toolbarOption,
        reloadDelary
      )

      // 开启只读模式
      editable.config.readOnly = true

      // 不要使用 reactive/ref,应该使用 shallowReactive/shallowRef 来接收 json 数据
      const formData = shallowReactive({
        json: [] as Descendant[],
        html: '',
      })

      function onEditableReloadBefore(inst: IDomEditor) {
        console.log('editable 即将重载: ' + new Date().toLocaleString())
      }

      function onToolbarReloadBefore(inst: Toolbar) {
        console.log('toolbar 即将重载: ' + new Date().toLocaleString())
      }

      return { editable, toolbar, formData, onEditableReloadBefore, onToolbarReloadBefore }
    },
  })
</script>
复制代码

2.3、Vue hook: useWangEditor

经过 useWangEditor 处理后,返回的 editabletoolbar 分别对应编辑器菜单栏的配置项,不过此时的配置项对象具备了响应式特性,我们可以直接修改 editable/toolbar 对应属性来 更新重载 编辑器。

如果传入的 editableOptiontoolbarOption 是响应式数据,内部将自动解除与之前的关联,也就意味着经过 useWangEditor 处理后得到的 editabletoolbar 配置对象,即使内容发生变化也不会触发之前的依赖更新!!!

/**
 * vue hook,用于实现编辑器配置项的动态绑定
 * @param {Object} editableOption 编辑器主体部分的配置
 * @param {Object} toolbarOption 菜单栏配置
 * @param {Number} reloadDelay 防抖时长,用于重载的延迟控制,单位:毫秒
 */
declare function useWangEditor(
  editableOption: EditorEditableOption | null = null,
  toolbarOption: EditorToolbarOption | null = null,
  reloadDelay: number = 365
): {
  editable: Required<EditorEditableOption>
  toolbar: Required<EditorToolbarOption>
  getEditable: () => IDomEditor | undefined
  getToolbar: () => Toolbar | undefined
  clearContent: () => void
  reloadEditor: () => void
}
复制代码

2.3.1、配置项:EditorEditableOption

/**
 * 编辑器配置项
 */
interface EditorEditableOption {
  /** 编辑器模式 */
  mode?: 'default' | 'simple'
  /** 编辑器初始化的默认内容 */
  defaultContent?: Descendant[] | string | null
  /** 编辑器配置,具体配置以官方为准 */
  config?: Partial<IEditorConfig>
  /** v-model/v-model:html 数据同步的防抖时长,默认值:3650,单位:毫秒 */
  delay?: number
  /**
   * 编辑器创建时默认内容的优先级排序,默认值:true。
   * true:v-model > v-model:html > defaultContent。
   * false: defaultContent > v-model > v-model:html。
   */
  extendCache?: boolean
}
复制代码

2.3.2、配置项:EditorToolbarOption

/**
 * 菜单栏的配置项
 */
interface EditorToolbarOption {
  mode?: 'default' | 'simple'
  config?: Partial<IToolbarConfig>
}
复制代码

2.4、动态修改配置

const { editable, toolbar } = useWangEditor()

editable.config.placeholder = '新的 placeholder'

// 切换为只读模式
editable.config.readOnly = true

toolbar.mode = 'simple'
复制代码

2.4.1、数据优先:EditorEditableOption.extendCache

v-model/v-model:htmldefaultContent 同时使用的时候,我们可以使用 extendCache 配置项来控制重载后编辑器的默认内容。

extendCahcetrue 时,编辑器创建/重载时显示内容的优先级为:v-model > v-model:html > defaultContent

extendCachefalse 时,编辑器创建/重载时显示内容的优先级为:defaultContent > v-model > v-model:htmlfalse 模式下可能会造成数据的丢失,因此在编辑器重载前一定要做好数据的保存工作,我们可以配置 reloadbefore 事件来进行数据的保存。

2.4.2、默认值:EditorEditableOption.defaultContent

defaultContent 的变更默认情况下是不会触发编辑器的重载的,如果需要将 defaultContent 内容直接显示出来,我们需要通过 reloadEditor 来强制重载编辑器。并且我们需要注意 extendCache 对重载后编辑器默认内容的影响。

const { editable, toolbar, reloadEditor } = useWangEditor()

onMounted(() => {
  setTimeout(() => {
    // 当你进行了 v-model/v-model:html 绑定时,如果你想在编辑器重载后将新设
    // 置的默认值显示为编辑器的默认内容,那么你需要设置 extendCache 为 false,
    // 这会导致编辑器内容的丢失,可以合理搭配 reloadbefore 事件进行处理
    editable.extendCache = false

    // 然后再修改配置
    editable.defaultContent = [{ type: 'header1', children: [{ text: '标题一' }] }]

    // 同时还支持字符串形式的 JSON
    editable.defaultContent = '[{"type":"header1","children":[{"text":"标题一"}]}]'

    // 针对 HTML 字符串也做了兼容(不推荐使用,有缺陷)
    editable.defaultContent = '<h1>标题一</h1><p>段落</p>'

    // 最后,你还需要强制重载编辑器
    reloadEditor()
  }, 5000)
})
复制代码

2.5、编辑器/菜单栏 重载

EditorEditableOption.modeEditorEditableOption.config.hoverbarKeysEditorEditableOption.config.maxLengthEditorEditableOption.config.customPaste 这几个配置项的变更会触发编辑器的重载,其它的 EditorEditableOption 配置项仅支持动态配置,但并不会触发重载,这能避免不必要的资源消耗。如果你需要强制重载编辑器,还提供了 reloadEditor API 来供使用者手动触发。

EditorEditableOption 不同的是,EditorToolbarOption 的的任意选项发生变化,都会触发菜单栏的重载。

const { reloadEditor } = useWangEditor()

// 强制重载编辑器
reloadEditor()
复制代码

2.5.1、重载之前:reloadbefore 事件

在编辑器重载之前,会触发 reloadbefore 事件。

<template>
  <editor-toolbar :option="toolbar" @reloadbefore="onToolbarReloadBefore" />
  <editor-editable v-model="formData.json" :option="editable" @reloadbefore="onEditableReloadBefore" />
</template>

<script lang="ts">
  import axios from 'axiios'
  import { Descendant } from 'slate'
  import { EditorEditable, EditorToolbar, useWangEditor } from 'xxx/wangeditor'
  import { defineComponent, shallowReactive } from 'vue'

  export default defineComponent({
    components: { EditorToolbar, EditorEditable },
    setup() {
      const { editable, toolbar, reloadEditor } = useWangEditor()

      const formData = shallowReactive({
        json: [] as Descendant[],
      })

      function onEditableReloadBefore(inst: IDomEditor) {
        window.alert('editable 即将重载')
        console.log('editable 即将重载: ' + new Date().toLocaleString())
        // 提交数据
        axios.post('xxx/xxx', formData)
      }

      function onToolbarReloadBefore(inst: Toolbar) {
        window.alert('toolbar 即将重载')
        console.log('toolbar 即将重载: ' + new Date().toLocaleString())
      }

      return { editable, toolbar, formData, onEditableReloadBefore, onToolbarReloadBefore }
    },
  })
</script>
复制代码

2.6、清除内容

不仅会清除编辑器内容,还会同步 v-model/v-model:html 数据

const { clearContent } = useWangEditor()

clearContent()
复制代码

2.7、获取菜单栏实例

const { getToolbar } = useWangEditor()

const toolbarInstance: Toolbar | undefined = getToolbar()
if (toolbarInstance) {
  // do somthing
} else {
  // do somthin
}
复制代码

2.8、获取编辑器实例

const { getEditable } = useWangEditor()

const editableInstance: IDomEditor | undefined = getEditable()
if (editableInstance) {
  console.log(editableInstance.children)
} else {
  console.error('编辑器未实例化')
}
复制代码

2.9、关于对 v-model 的支持

EditorEditable 同时支持 v-modelv-model:html 两种形式的数据绑定,分别对应 json arrayhtml string 两种格式的数据。两种格式可以同时绑定,也可以单独只绑定 v-modelv-model:html 之一,亦可以不进行数据绑定。

不推荐只进行 v-model:html 绑定,有无法避免的缺陷!!! 并且需要注意 extendCache 可能存在的影响!!!

同时,当我们进行 v-model 绑定时,推荐使用 shallowReactive/shallowRef 来缓存 json array 数据。如果你执意使用 reactive/ref 进行数据缓存,那么在运行时出现未知错误,那么你可能找不到问题所在。重要!重要!!重要!!!

<template>
  <editor-editable :option="editable" v-model="formData.json" v-model:html="formData.html" />
</template>

<script lang="ts">
  import { Descendant } from 'slate'
  import { useWangEditor } from '@we/wangeditor'
  import { defineComponent, shallowReactive } from 'vue'

  export default defineComponent({
    setup() {
      const { editable } = useWangEditor()

      const formData = shallowReactive({
        json: [] as Descendant[],
        html: '',
      })

      return { editable, formData }
    },
  })
</script>
复制代码

<template>
  <editor-editable :option="editable" v-model="jsonData" v-model:html="htmlData" />
</template>

<script lang="ts">
  import { Descendant } from 'slate'
  import { useWangEditor } from '@we/wangeditor'
  import { defineComponent, shallowRef } from 'vue'

  export default defineComponent({
    setup() {
      const { editable } = useWangEditor()

      const jsonData = shallowRef<Descendant[]>([])

      const htmlData = ref('')

      return { editable, jsonData, htmlData }
    },
  })
</script>
复制代码

3、总结

此次封装组件给我最大的感受莫过于:成也响应式 API,bug 也来自响应式 API。因此,如果你也在使用第三方库,且这些库中使用到了响应式数据,那么一定要合理使用 toRawmarkRawshallowReactiveshallowReadonlyunrefshallowRef 这些 API 来解除数据的响应式特性。否则,运行时 bug 可不是那么好解决的。

针对响应式特性造成的运行时 bug,wangEditor 官方给出的解决示例(issues:262)是对数据进行深度克隆,虽然不够优雅,但效果却是格外的好用,也足够简单。当然,在本组件中只要你按照文档来,就不用考虑相关问题。

4、最后

由于个中缘由,目前不发布 npm 包,如有需要可以 GitHub 自取(仅一个文件),如果觉得好用不妨给个 star

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改