Vue3 + wangEditor 5.x 全栈整合实战:从零构建富文本编辑器与文件上传系统
如果你正在开发一个需要富文本编辑功能的中后台管理系统或博客平台,wangEditor 5.x 结合Vue3的组合式API会是一个高效的选择。不同于市面上其他臃肿的编辑器,wangEditor以轻量、易扩展著称,特别适合国内开发环境。下面我将分享如何在Vue3项目中完整集成wangEditor 5.x,并实现前后端分离的文件上传方案。
1. 环境准备与基础集成
1.1 创建Vue3项目与安装依赖
首先确保你的开发环境已经配置好Node.js(建议版本16+)和Vue CLI。使用以下命令创建一个新的Vue3项目:
npm init vue@latest vue3-wangeditor-demo cd vue3-wangeditor-demo npm install接下来安装wangEditor的Vue3专用包:
npm install @wangeditor/editor @wangeditor/editor-for-vue --save1.2 基础编辑器组件封装
在src/components目录下创建RichTextEditor.vue文件,这是我们的核心编辑器组件:
<template> <div class="editor-container"> <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" /> <Editor v-model="valueHtml" :defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" /> </div> </template> <script setup> import '@wangeditor/editor/dist/css/style.css' import { ref, shallowRef, onBeforeUnmount } from 'vue' import { Editor, Toolbar } from '@wangeditor/editor-for-vue' // 编辑器实例必须用shallowRef const editorRef = shallowRef() const valueHtml = ref('<p>初始内容</p>') const toolbarConfig = { excludeKeys: ['group-video'] // 排除不需要的功能 } const editorConfig = { placeholder: '请输入内容...', MENU_CONF: {} } const handleCreated = (editor) => { editorRef.value = editor } onBeforeUnmount(() => { const editor = editorRef.value if (editor) editor.destroy() }) </script> <style scoped> .editor-container { border: 1px solid #ddd; border-radius: 4px; overflow: hidden; } </style>关键点说明:
- 使用
shallowRef而非ref存储编辑器实例,避免不必要的响应式开销 onBeforeUnmount生命周期中必须销毁编辑器实例,防止内存泄漏- 通过
excludeKeys可以灵活控制工具栏显示的功能项
2. 深度配置与自定义功能
2.1 工具栏自定义配置
wangEditor允许高度自定义工具栏。以下是常用的配置示例:
const toolbarConfig = { toolbarKeys: [ 'headerSelect', 'bold', 'italic', 'underline', 'color', 'bgColor', 'fontSize', 'fontFamily', 'lineHeight', '|', 'bulletedList', 'numberedList', 'todo', '|', 'emotion', 'insertLink', 'uploadImage', 'insertTable', 'codeBlock', 'divider', '|', 'undo', 'redo', '|', 'fullScreen' ], excludeKeys: ['group-video', 'insertVideo'] }2.2 编辑器内容变化监听
在实际应用中,我们通常需要实时获取编辑器内容:
<script setup> // ...其他代码... watch(valueHtml, (newVal) => { console.log('内容变化:', newVal) // 可以在这里触发自动保存等操作 }) </script>3. 文件上传功能实现
3.1 前端上传配置
修改editorConfig中的MENU_CONF配置,实现图片上传:
const editorConfig = { placeholder: '请输入内容...', MENU_CONF: { uploadImage: { fieldName: 'file', server: 'http://your-api-domain.com/api/upload', maxFileSize: 5 * 1024 * 1024, // 5M allowedFileTypes: ['image/*'], timeout: 10 * 1000, // 10秒 customInsert(res, insertFn) { // 处理上传结果 if (res.errno === 0) { insertFn(res.data.url, '', res.data.url) } else { console.error('上传失败:', res.message) } } } } }3.2 SpringBoot后端实现
创建一个简单的文件上传接口:
@RestController @RequestMapping("/api") public class FileUploadController { @PostMapping("/upload") public Map<String, Object> uploadImage( @RequestParam("file") MultipartFile file, HttpServletRequest request) { Map<String, Object> result = new HashMap<>(); try { // 1. 文件校验 if (file.isEmpty()) { throw new RuntimeException("文件不能为空"); } // 2. 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String fileExt = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFilename = UUID.randomUUID().toString() + fileExt; // 3. 确定存储路径 String uploadDir = request.getServletContext().getRealPath("/uploads/"); File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } // 4. 保存文件 File dest = new File(uploadDir + newFilename); file.transferTo(dest); // 5. 构建返回结果 String fileUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/uploads/" + newFilename; result.put("errno", 0); Map<String, String> data = new HashMap<>(); data.put("url", fileUrl); data.put("alt", originalFilename); data.put("href", fileUrl); result.put("data", data); } catch (Exception e) { result.put("errno", 1); result.put("message", e.getMessage()); } return result; } }application.yml配置:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB4. 高级功能与性能优化
4.1 自定义扩展菜单
wangEditor支持自定义菜单项。例如添加一个"插入特定模板"的按钮:
import { DomEditor } from '@wangeditor/editor' function insertTemplate(editor) { if (editor == null) return editor.insertText('【这里是模板内容】') } const toolbarConfig = { insertKeys: { index: 5, // 插入位置 keys: ['insertTemplate'] } } // 注册自定义菜单 Editor.registerMenu('insertTemplate', { title: '插入模板', iconSvg: '<svg>...</svg>', // 你的SVG图标 tag: 'button', exec(editor) { insertTemplate(editor) } })4.2 性能优化建议
- 按需加载:如果不需要所有功能,可以只引入必要的模块:
import { Boot } from '@wangeditor/editor' import module from '@wangeditor/module-name' // 具体模块 Boot.registerModule(module)- 懒加载编辑器:在需要时才加载编辑器组件:
<template> <button @click="showEditor = true">显示编辑器</button> <RichTextEditor v-if="showEditor" /> </template> <script setup> import { ref } from 'vue' const showEditor = ref(false) </script>- 内容缓存:使用防抖函数定期保存编辑器内容:
import { debounce } from 'lodash-es' const autoSave = debounce((content) => { localStorage.setItem('editor-content', content) }, 2000) watch(valueHtml, (newVal) => { autoSave(newVal) })5. 常见问题解决方案
5.1 编辑器初始化问题
问题:编辑器无法正常显示或报错
解决方案:
- 确保正确引入了CSS文件
- 检查编辑器实例是否使用了
shallowRef - 确认组件销毁时调用了
editor.destroy()
5.2 图片上传失败处理
问题:图片上传接口返回格式不符合预期
解决方案:
- 确保后端返回的JSON格式包含
errno和data.url字段 - 可以通过
customInsert自定义处理逻辑:
customInsert(res, insertFn) { // 兼容不同后端返回格式 const url = res.data?.url || res.url if (res.code === 200 || res.errno === 0) { insertFn(url, '', url) } else { alert(res.message || '上传失败') } }5.3 内容样式问题
问题:编辑器中的内容在前端展示时样式不一致
解决方案:
- 引入wangEditor的内容样式:
@import '@wangeditor/editor/dist/css/style.css'; /* 内容展示区域 */ .content-container { /* 重置一些样式 */ img { max-width: 100%; } table { border-collapse: collapse; } /* 其他样式... */ }- 或者使用编辑器提供的
toHtml方法:
import { DomEditor } from '@wangeditor/editor' const html = DomEditor.toHtml(editorRef.value)在实际项目中,我发现最常遇到的问题往往与编辑器实例的生命周期管理有关。特别是在使用Vue Router进行页面跳转时,一定要确保在组件卸载前销毁编辑器实例,否则可能导致内存泄漏。另外,对于内容较多的场景,建议实现自动保存功能,避免用户意外丢失编辑内容。