Vue3项目中深度封装wangEditor v5:从动态菜单配置到图片上传实战

在后台管理系统开发中,富文本编辑器的集成往往成为关键路径。不同于简单的API调用,一个真正可复用的富文本组件需要解决动态配置、状态管理和文件上传等工程化问题。本文将分享在Vue3项目中深度封装wangEditor v5的完整方案,特别针对CMS、OA等系统的实际需求。

1. 项目初始化与基础封装

1.1 创建可复用的编辑器组件

首先建立基础的组件结构,这将成为后续功能扩展的基石:

<template>
  <div class="editor-container">
    <Toolbar 
      :editor="editorInstance" 
      :defaultConfig="toolbarConfig"
      mode="default"
    />
    <Editor
      v-model="contentHtml"
      :defaultConfig="mergedConfig"
      mode="default"
      @onCreated="handleEditorCreated"
    />
  </div>
</template>

<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { shallowRef, computed } from 'vue'

const props = defineProps({
  modelValue: String,
  config: Object,
  toolbarKeys: Array
})

const emit = defineEmits(['update:modelValue'])

const editorInstance = shallowRef(null)
const contentHtml = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
</script>

这种封装方式实现了:

  • 通过 v-model 实现双向数据绑定
  • 使用 shallowRef 优化编辑器实例性能
  • 允许外部传入配置进行自定义

1.2 动态菜单配置策略

实际项目中,不同场景需要不同的工具栏配置。通过 toolbarKeys 实现动态菜单管理:

const toolbarConfig = computed(() => ({
  toolbarKeys: props.toolbarKeys || [
    'headerSelect',
    'bold',
    'italic',
    'uploadImage',
    'insertTable',
    'codeBlock'
  ],
  excludeKeys: ['group-video'] // 排除不需要的菜单项
}))

典型应用场景示例:

场景类型 推荐配置 适用业务
基础内容编辑 文本样式+图片上传 新闻发布
全功能模式 包含表格、代码块等复杂功能 技术文档编辑
只读模式 空数组 + disable()方法 内容审核界面

2. 深度集成图片上传功能

2.1 完整的文件上传实现

不同于简单的演示代码,生产环境需要处理更多边界情况:

const editorConfig = computed(() => ({
  ...props.config,
  MENU_CONF: {
    uploadImage: {
      maxFileSize: 5 * 1024 * 1024, // 5MB
      allowedFileTypes: ['image/*'],
      customUpload: async (file, insertFn) => {
        try {
          const formData = new FormData()
          formData.append('file', file)
          formData.append('source', 'editor')
          
          const { data } = await uploadService(formData)
          
          insertFn(
            data.url, 
            file.name,
            `data-id="${data.id}"` // 添加自定义属性
          )
        } catch (error) {
          console.error('上传失败:', error)
          // 可添加用户提示
        }
      }
    }
  }
}))

关键优化点:

  • 添加文件类型和大小限制
  • 异常处理机制
  • 为图片添加元数据属性
  • 统一的API服务封装

2.2 上传状态管理进阶方案

为提升用户体验,可以实现上传进度显示:

customUpload: async (file, insertFn) => {
  const progress = ref(0)
  showProgress.value = true
  
  const cancelToken = new axios.CancelToken(c => {
    cancelUpload.value = c
  })
  
  try {
    const { data } = await uploadService({
      file,
      onUploadProgress: (e) => {
        progress.value = Math.round((e.loaded / e.total) * 100)
      },
      cancelToken
    })
    
    insertFn(data.url, file.name)
  } finally {
    hideProgress()
  }
}

配套的UI组件:

<template>
  <div v-if="showProgress" class="upload-progress">
    <div class="progress-bar" :style="{ width: `${progress}%` }"></div>
    <button @click="cancelUpload">取消</button>
  </div>
</template>

3. 状态管理与性能优化

3.1 编辑器实例的生命周期

正确处理组件的挂载和卸载至关重要:

onBeforeUnmount(() => {
  if (editorInstance.value && !editorInstance.value.isDestroyed) {
    editorInstance.value.destroy()
  }
})

const handleEditorCreated = (editor) => {
  editorInstance.value = editor
  
  // 根据props初始化状态
  if (props.disabled) {
    editor.disable()
  }
  
  // 注册自定义插件
  registerCustomPlugins(editor)
}

常见内存泄漏场景:

  • 未正确销毁编辑器实例
  • 未清理事件监听器
  • 未取消未完成的网络请求

3.2 响应式状态同步

实现父组件与编辑器状态的双向同步:

watch(() => props.disabled, (val) => {
  if (!editorInstance.value) return
  val ? editorInstance.value.disable() : editorInstance.value.enable()
})

watch(() => props.modelValue, (val) => {
  if (!editorInstance.value || val === editorInstance.value.getHtml()) return
  editorInstance.value.setHtml(val || '')
})

4. 业务场景深度适配

4.1 协同编辑解决方案

在多人协作场景下,需要额外处理冲突问题:

// 在组件中
const handleChange = debounce((editor) => {
  const content = editor.getHtml()
  socketService.emit('editor-update', {
    docId: props.docId,
    content
  })
}, 500)

onMounted(() => {
  socketService.on('remote-update', (content) => {
    if (editorInstance.value && content !== editorInstance.value.getHtml()) {
      editorInstance.value.setHtml(content)
    }
  })
})

关键考虑因素:

  • 使用防抖减少网络请求
  • 处理冲突的合并策略
  • 离线编辑支持

4.2 内容审核集成方案

对于需要审核的场景,可以扩展编辑器功能:

const extendEditor = (editor) => {
  editor.registerModule('audit', {
    highlightSensitiveWords(words) {
      // 实现敏感词高亮
    },
    getStats() {
      return {
        wordCount: editor.getText().length,
        imageCount: editor.getElemsByType('image').length
      }
    }
  })
}

5. 样式定制与主题适配

5.1 深度样式定制

通过CSS变量实现主题适配:

.editor-container {
  --editor-border: 1px solid #dcdfe6;
  --editor-toolbar-bg: #f8f9fa;
  --editor-active-btn: #409eff;
}

.w-e-toolbar {
  background: var(--editor-toolbar-bg) !important;
  border-bottom: var(--editor-border) !important;
}

.w-e-text-container {
  border: var(--editor-border) !important;
}

5.2 响应式布局优化

确保编辑器在不同设备上的显示效果:

const handleResize = () => {
  if (editorInstance.value) {
    const container = editorInstance.value.$el.parentElement
    const height = window.innerHeight - container.getBoundingClientRect().top - 20
    editorInstance.value.$el.style.height = `${height}px`
  }
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
  nextTick(handleResize)
})

6. 测试与调试策略

6.1 单元测试重点

针对富文本组件的测试策略:

describe('WangEditor组件', () => {
  it('应该正确初始化编辑器', async () => {
    const wrapper = mount(Component, {
      props: { modelValue: '<p>测试内容</p>' }
    })
    await nextTick()
    expect(wrapper.find('.w-e-text-container').exists()).toBe(true)
  })
  
  it('应该处理disable状态', async () => {
    const wrapper = mount(Component, {
      props: { disabled: true }
    })
    await nextTick()
    expect(wrapper.vm.editorInstance.isDisabled).toBe(true)
  })
})

6.2 常见问题排查

开发者可能遇到的典型问题:

问题现象 可能原因 解决方案
编辑器无法初始化 CSS未正确引入 检查@wangeditor/editor/dist/css
图片上传失败 CORS配置问题 检查服务器Access-Control头
内存泄漏 未调用destroy() 确保在onBeforeUnmount中销毁
响应式数据不同步 直接修改了编辑器DOM 使用setHtml/getHtml API

在项目实践中,我们发现将编辑器实例的管理提升到Pinia/Vuex中能够更好地处理复杂场景下的状态同步问题。特别是在需要多个编辑器实例协同工作的场景下,集中式状态管理可以显著降低组件间的耦合度。

更多推荐