1. 为什么我们需要封装一个自己的WangEditor组件?
如果你在Vue项目里用过富文本编辑器,尤其是WangEditor,你肯定有过这样的体验:每次新建一个页面,都要把那一大坨配置代码复制粘贴一遍,什么工具栏配置、上传图片的接口地址、各种回调函数……更别提如果项目里有十几个地方要用到编辑器,哪天上传接口地址变了,或者要统一调整一下最大上传文件大小,你就得一个个文件去改,想想都头大。
我最早也是这么干的,直到在一个中后台项目里,表单、详情页、评论回复框加起来用了快二十个编辑器实例,维护起来简直是一场噩梦。后来我下定决心,必须把它封装成一个独立的、可复用的Vue组件。封装的好处太明显了:一处配置,处处生效;逻辑集中,便于维护;接口统一,调用简单。而且,一个好的封装还能帮你处理掉很多潜在的坑,比如编辑器实例的创建与销毁、Vue响应式数据与编辑器内部状态的同步、以及跨Vue版本的兼容性问题。
你可能担心,封装会不会很复杂?其实不然。WangEditor官方已经为Vue提供了很好的适配包(@wangeditor/editor-for-vue),我们要做的,就是基于它,用Vue组件化的思想,把那些零散的配置和逻辑“打包”起来,对外暴露几个简单明了的属性(props)和方法(methods)。这样一来,无论是在Vue2的老项目,还是Vue3的新项目里,你都能像使用一个普通的输入框一样,轻松地引入一个功能强大且稳定的富文本编辑器。
所以,这篇文章的目的,就是带你一步步实现这个“打包”过程。我会从最基础的安装配置讲起,重点攻克最常用也最麻烦的多图上传功能,并确保我们的封装方案能同时兼容Vue2和Vue3。最终,你会得到一个开箱即用、功能完备的 WangEditor 组件,直接复制到你的项目里就能跑起来。
2. 项目环境准备与依赖安装
万事开头先装包。这里有个小细节需要注意:Vue2和Vue3项目在安装WangEditor的Vue适配包时,版本选择上略有不同。虽然Vue3兼容大部分Vue2的语法,但底层响应式系统变了,对应的适配包版本也需要匹配。
首先,无论Vue2还是Vue3,核心的编辑器包都是同一个:
# 安装编辑器核心包
npm install @wangeditor/editor@5.1.15 --save
接下来是Vue适配包。我建议在安装前,先用 npm view 命令查看一下有哪些版本可用,避免装到不兼容的版本。
# 查看 @wangeditor/editor-for-vue 的所有版本
npm view @wangeditor/editor-for-vue versions --json
这个命令会列出一串版本号。对于 Vue2项目,你可以选择 5.1.12 这个我实测稳定的版本:
# Vue2 项目安装
npm install @wangeditor/editor-for-vue@5.1.12 --save
对于 Vue3项目,你需要安装支持Vue3的更高版本,比如 5.1.23 或以上。请务必查看官方文档或npm页面确认最新兼容版本。
# Vue3 项目安装 (版本号请以官方最新为准)
npm install @wangeditor/editor-for-vue@^5.1.23 --save
为什么这么麻烦?因为 @wangeditor/editor-for-vue 这个包内部使用了Vue的Composition API或Options API,不同版本针对Vue2和Vue3做了不同的内部实现。装错了版本,可能会导致组件无法渲染,或者响应式失效。我就在一个Vue3项目里误装了老版本,折腾了半天才发现是这里的问题。
安装完成后,你的 package.json 里应该会新增这两项依赖。接下来,我们就可以着手创建组件文件了。
3. 核心组件封装:从零搭建WangEditor.vue
封装的核心思想是:内部消化复杂配置,对外提供简洁接口。我们将创建一个 WangEditor.vue 单文件组件,它负责管理编辑器实例的生命周期、配置上传、处理内容变化等所有脏活累活。
3.1 组件模板与基本结构
我们先搭建组件的骨架。模板部分很简单,主要就是官方提供的 Toolbar(工具栏)和 Editor(编辑区域)两个组件。
<template>
<div class="wang-editor-container">
<!-- 工具栏区域 -->
<Toolbar
class="editor-toolbar"
:editor="editorInstance"
:defaultConfig="toolbarConfig"
/>
<!-- 编辑区域 -->
<Editor
class="editor-content"
:style="{ minHeight: editorMinHeight }"
:defaultConfig="mergedEditorConfig"
v-model="localContent"
@onCreated="handleEditorCreated"
@onChange="handleEditorChange"
/>
</div>
</template>
<script>
// 导入Vue适配的编辑器组件
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
name: 'WangEditor',
components: { Editor, Toolbar },
props: {
// 外部传入的配置参数
modelValue: {
type: String,
default: ''
},
editorParams: {
type: Object,
default: () => ({})
}
},
data() {
return {
editorInstance: null, // 编辑器实例,最重要的对象
localContent: '', // 用于内部双向绑定的内容
// 工具栏配置,可以在这里定制显示的按钮
toolbarConfig: {
// 例如,排除某些菜单
// excludeKeys: ['group-video', 'emotion']
},
// 编辑器的默认配置,会上传配置等会合并到这里
editorConfig: {
placeholder: '请输入内容...',
MENU_CONF: {} // 菜单配置,上传图片的关键在这里
}
}
},
computed: {
// 合并默认配置和外部传入的配置
mergedEditorConfig() {
return {
...this.editorConfig,
placeholder: this.editorParams.placeholder || this.editorConfig.placeholder,
// 其他需要合并的配置...
}
},
// 编辑器最小高度
editorMinHeight() {
return this.editorParams.height || '300px'
}
},
watch: {
// 监听外部传入的富文本内容变化,同步到编辑器
modelValue(newVal) {
if (newVal !== this.localContent && this.editorInstance) {
this.localContent = newVal
}
},
// 深度监听外部参数对象,动态更新配置(如上传地址)
editorParams: {
handler(newVal) {
if (newVal && newVal.uploadImageUrl) {
this.setupUploadConfig(newVal.uploadImageUrl)
}
},
immediate: true,
deep: true
}
},
methods: {
// 编辑器创建成功后的回调
handleEditorCreated(editor) {
this.editorInstance = editor
// 这里可以做一些编辑器创建后的初始化操作
},
// 编辑器内容变化时的回调
handleEditorChange(editor) {
const html = editor.getHtml()
// 将内容更新同步给父组件
this.$emit('update:modelValue', html)
}
},
// 组件销毁前,务必销毁编辑器实例,防止内存泄漏
beforeUnmount() {
if (this.editorInstance) {
this.editorInstance.destroy()
this.editorInstance = null
}
}
}
</script>
<style lang="scss" scoped>
.wang-editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
.editor-toolbar {
border-bottom: 1px solid #dcdfe6;
}
.editor-content {
padding: 12px;
}
}
</style>
这个基础版本已经具备了双向绑定、参数监听和实例销毁的功能。但还缺少灵魂——图片上传。别急,我们马上来攻克它。
3.2 处理一个隐蔽的坑:拼写检查与自动聚焦
在真正开始上传功能前,我想先分享两个实际开发中遇到的“小坑”,它们不致命但很烦人。
第一个是拼写检查。浏览器默认会对可编辑区域进行英文拼写检查,在富文本编辑器里就会显示难看的红色波浪线。虽然WangEditor配置项里可以设置 spellcheck: false,但在某些浏览器(比如Chrome的某些版本)下,光配置不生效。我找到的解决办法是在组件挂载后,直接操作DOM元素来关闭它。
第二个是移动端自动聚焦。在移动设备上,如果编辑器自动获得焦点,会触发虚拟键盘弹出,影响体验。我们需要阻止这个行为。
我在组件的 mounted 生命周期里统一处理了它们:
mounted() {
// 1. 关闭拼写检查的红色波浪线
this.$nextTick(() => {
const scrollElements = document.querySelectorAll('.w-e-scroll')
scrollElements.forEach(el => {
if (el.children[0]) {
el.children[0].setAttribute('spellcheck', 'false')
}
})
})
// 2. 移除自动焦点,防止移动端虚拟键盘自动弹出
if (document.activeElement && document.activeElement.blur) {
document.activeElement.blur()
}
}
这两个操作属于“锦上添花”的优化,能让你的编辑器体验更上一层楼。
4. 重头戏:实现强大且稳健的多图上传功能
图片上传是富文本编辑器最核心、也最容易出问题的功能。WangEditor的上传配置非常灵活,但也意味着我们需要考虑更多细节:文件大小限制、格式限制、多文件上传、上传进度、成功/失败回调、服务端返回格式约定等等。
4.1 前端上传配置详解
我们在 editorConfig.MENU_CONF 中配置 uploadImage 菜单。我将配置拆解成一个独立的方法 setupUploadConfig,这样逻辑更清晰。
methods: {
setupUploadConfig(uploadUrl) {
// 确保 MENU_CONF 存在
if (!this.editorConfig.MENU_CONF) {
this.editorConfig.MENU_CONF = {}
}
this.editorConfig.MENU_CONF['uploadImage'] = {
// ------------ 基础配置 ------------
server: uploadUrl, // 上传接口地址,由父组件传入
fieldName: 'files', // 表单文件字段名,与服务端接收参数名对应
maxFileSize: 2 * 1024 * 1024, // 单个文件最大 2MB
maxNumberOfFiles: 10, // 一次最多选10个文件
allowedFileTypes: ['image/*'], // 允许所有图片类型
timeout: 10000, // 10秒超时
withCredentials: false, // 跨域是否发送cookie,根据你的项目设置
// ------------ 自定义参数与请求头 ------------
meta: {
// 这里可以传递一些额外的认证信息,比如token
token: this.getAuthToken(),
from: 'wangeditor'
},
headers: {
// 设置自定义请求头
'X-Requested-With': 'XMLHttpRequest'
},
// ------------ 生命周期回调函数 ------------
// 选择文件后,上传之前触发。可以在这里做最后校验或修改文件信息。
onBeforeUpload(file) {
console.log(`${file.name} 开始上传`)
// 如果返回 false,会阻止上传
// 如果返回 File 对象,会使用返回的这个新对象
return file
},
// 上传进度回调,可以用来做进度条
onProgress(progress) {
// progress 是一个 0-100 的数字
console.log('当前进度:', progress)
},
// 单个文件上传成功
onSuccess(file, res) {
// 重点:这里需要根据你服务端返回的实际数据结构来解析!
// WangEditor 期望的返回值格式是 { errno: 0, data: { url: '图片地址' } }
console.log(`${file.name} 上传成功`, res)
// 通常你不需要在这里做任何事,编辑器会自动处理。
// 但如果服务端返回格式不一致,你可能需要在这里转换格式。
},
// 单个文件上传失败
onFailed(file, res) {
console.error(`${file.name} 上传失败`, res)
// 可以在这里触发全局错误提示
this.$message.error(`文件 ${file.name} 上传失败`)
},
// 上传过程发生错误(网络错误、超时等)
onError(file, err, res) {
console.error(`${file.name} 上传出错`, err, res)
this.$message.error('网络错误,请重试')
}
}
}
}
这里有几个关键点需要你根据实际情况调整:
fieldName:必须和服务端接收文件参数的字段名一致。我习惯用files,也有人用file或images。maxFileSize和allowedFileTypes:这是前端的第一道防线,可以有效拦截不合法文件,减轻服务端压力。onSuccess回调:这是最容易出错的地方。WangEditor要求服务端返回一个非常特定的JSON格式:{ errno: 0, data: { url: 'https://xxx.com/image.jpg' } }。errno必须是数字0表示成功,data是一个对象,里面至少包含url字段。如果你的后端同事返回的格式不一样(比如{ code: 200, url: '...' }),你就必须在onSuccess回调里手动转换成编辑器能识别的格式。
4.2 服务端实现(Java Spring Boot示例)
为了让你前后端联调更顺畅,这里给出一个完整的Spring Boot控制器示例。它接收多文件,保存到本地,并返回WangEditor期望的格式。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.*;
@RestController
@RequestMapping("/api")
public class FileUploadController {
@Value("${app.upload.dir:/tmp/uploads}") // 从配置文件中读取上传路径
private String uploadDir;
@PostMapping("/upload/image")
// @CrossOrigin // 如果前端是跨域请求,需要添加此注解或使用全局配置
public Map<String, Object> uploadImage(@RequestParam("files") List<MultipartFile> files) {
Map<String, Object> result = new HashMap<>();
List<Map<String, String>> dataList = new ArrayList<>();
try {
for (MultipartFile file : files) {
// 1. 生成唯一文件名,防止覆盖
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = UUID.randomUUID().toString().replace("-", "") + fileExtension;
// 2. 确定文件保存路径
File destDir = new File(uploadDir);
if (!destDir.exists()) {
destDir.mkdirs();
}
File destFile = new File(destDir, newFilename);
// 3. 保存文件
file.transferTo(destFile);
// 4. 构造返回给前端的数据项
// 假设你的静态资源可以通过 http://your-domain.com/uploads/ 访问
String fileUrl = "/uploads/" + newFilename; // 或者完整的绝对URL
Map<String, String> fileInfo = new HashMap<>();
fileInfo.put("url", fileUrl);
fileInfo.put("alt", originalFilename); // 可选
fileInfo.put("href", ""); // 可选,图片链接
dataList.add(fileInfo);
}
// 5. 严格按照WangEditor要求的格式返回
result.put("errno", 0); // 关键!必须是数字0
result.put("data", dataList);
} catch (IOException e) {
e.printStackTrace();
result.put("errno", 1); // 非0即表示失败
result.put("message", "文件上传失败");
}
return result;
}
}
对应的,你需要在 application.yml 中配置上传路径,并设置静态资源映射,让保存的图片能被外部访问到。
# application.yml
app:
upload:
dir: D:/project_uploads/ # Windows路径示例,Linux/Mac请修改
# 静态资源映射配置类
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${app.upload.dir}")
private String uploadDir;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将 /uploads/** 路径映射到本地文件目录
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir + File.separator);
}
}
这样,一个支持多图上传、带基础错误处理的后端接口就准备好了。前端组件里的 uploadImageUrl 就可以配置为 http://你的域名/api/upload/image。
5. 在父组件中轻松调用与进阶技巧
封装好后,在父组件中使用就变得极其简单。
5.1 基础使用
<template>
<div>
<h3>发布文章</h3>
<WangEditor
v-model="articleContent"
:editor-params="editorConfig"
/>
<button @click="submit">提交</button>
</div>
</template>
<script>
import WangEditor from '@/components/WangEditor.vue'
export default {
components: { WangEditor },
data() {
return {
articleContent: '<p>这里是初始内容</p>',
editorConfig: {
placeholder: '请输入正文...',
uploadImageUrl: '/api/upload/image', // 上传地址
height: '500px'
}
}
},
methods: {
submit() {
console.log('提交的HTML内容:', this.articleContent)
// 这里可以发送到后端
}
}
}
</script>
通过 v-model 就能轻松获取和设置富文本内容,所有复杂的上传逻辑都隐藏在组件内部。
5.2 进阶:通过Ref调用组件方法
有时我们可能需要以编程方式操作编辑器,比如清空内容、插入特定内容、或者获取纯文本。我们可以在子组件中暴露一些方法,然后通过 ref 来调用。
在 WangEditor.vue 组件中,我们添加以下方法:
methods: {
// ... 其他方法
// 获取纯文本
getPlainText() {
return this.editorInstance ? this.editorInstance.getText() : ''
},
// 获取HTML
getHtml() {
return this.editorInstance ? this.editorInstance.getHtml() : ''
},
// 清空内容
clear() {
if (this.editorInstance) {
this.editorInstance.clear()
this.localContent = ''
}
},
// 插入HTML
insertHtml(html) {
if (this.editorInstance) {
const editor = this.editorInstance
editor.restoreSelection() // 恢复选区
editor.insertHtml(html)
}
}
}
在父组件中,就可以这样使用:
<template>
<div>
<WangEditor ref="myEditor" v-model="content" :editor-params="config"/>
<button @click="handleGetText">获取纯文本</button>
<button @click="handleClear">清空</button>
</div>
</template>
<script>
export default {
methods: {
handleGetText() {
const text = this.$refs.myEditor.getPlainText()
console.log('纯文本:', text)
},
handleClear() {
this.$refs.myEditor.clear()
}
}
}
</script>
5.3 性能优化:使用v-if控制显隐
最后是一个非常重要的性能优化点。如果你的编辑器组件在一个弹窗或标签页里,会频繁地创建和销毁,务必使用 v-if 而不是 v-show。
因为WangEditor实例包含大量的DOM操作和事件监听,单纯的 v-show(隐藏)无法销毁实例,会造成内存泄漏。而 v-if 在条件为假时,会完全销毁组件及其子组件。
<template>
<div>
<button @click="showEditor = !showEditor">切换编辑器</button>
<!-- 使用 v-if 确保彻底销毁 -->
<WangEditor v-if="showEditor" v-model="content" :editor-params="config"/>
</div>
</template>
这个坑是我在开发一个复杂标签页应用时踩到的,页面打开久了就会越来越卡,排查后发现就是因为编辑器实例没有正确销毁。改用 v-if 后问题迎刃而解。
6. 跨版本兼容与样式自定义
我们的封装方案天生就具备Vue2/Vue3兼容性。核心在于两点:一是我们使用了Vue2风格的Options API编写组件,这在Vue3中是完全被兼容的(除非你项目明确禁用);二是我们通过 @wangeditor/editor-for-vue 这个适配包来屏蔽底层差异。
对于Vue3项目,你只需要确保安装正确版本的适配包(如前面所述),然后使用方式完全一样。如果你的Vue3项目全面采用Composition API和 <script setup>,你可能希望组件也以同样的方式提供。这也很简单,你可以基于现有的逻辑,用Composition API重写一遍 WangEditor.vue,或者提供一个额外的适配版本。但就我的经验而言,在大多数混合项目中,当前这个基于Options API的组件已经足够好用且稳定。
关于样式,你可以通过覆盖CSS类名来深度定制编辑器的外观。组件的 <style> 部分已经给出了一些基础样式,比如边框、圆角。WangEditor有自己的一套CSS类名体系,例如 .w-e-text-container 是编辑区,.w-e-toolbar 是工具栏。你可以通过浏览器开发者工具查看元素类名,然后在你项目的全局样式文件或组件的scoped样式中进行覆盖。比如,想修改工具栏按钮的悬停颜色,可以这样写:
/* 在全局样式或组件内(非scoped下) */
.w-e-toolbar .w-e-menu:hover {
background-color: #f0f7ff;
}
封装和调试的过程,其实就是不断与编辑器“对话”的过程。遇到问题,多看看官方文档,多利用浏览器控制台查看网络请求和错误信息,大部分问题都能找到答案。

1356

被折叠的 条评论
为什么被折叠?



