Vue3项目里,我是这样用wangEditor v5处理富文本的:从自定义菜单到图片上传
·
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中能够更好地处理复杂场景下的状态同步问题。特别是在需要多个编辑器实例协同工作的场景下,集中式状态管理可以显著降低组件间的耦合度。
更多推荐
所有评论(0)