一、前言
公告、文章、商品详情都需要富文本(加粗/图片/列表)。本篇集成国内最常用的 wangEditor 5:Vue3 官方适配层 + 图片上传配置 + 封装成组件。先说明:wangEditor 5 官方已停止维护但依然稳定可用;若求新可评估 Tiptap/Quill,本篇以企业存量最常见的 wangEditor 为准。
bash
npm i @wangeditor/editor @wangeditor/editor-for-vue@next
# ★ Vue3 必须装 @next 标签版本(默认 latest 是 Vue2 组件层)
二、核心组件结构
wangEditor 的 Vue 封装分两块:Toolbar(工具栏)+ Editor(编辑区),各占一行布局:
vue
<!-- src/components/RichEditor.vue -->
<template>
<div class="rich-editor">
<Toolbar class="toolbar" :editor="editorRef" :defaultConfig="toolbarConfig" mode="default" />
<Editor
class="editor"
v-model="valueHtml"
:defaultConfig="editorConfig"
mode="default"
style="height: 300px"
@onCreated="handleCreated"
/>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, onBeforeUnmount } from 'vue'
import { Toolbar, Editor } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css' // ★ 样式必须引入
// v-model:编辑内容(HTML 字符串)
const html = defineModel<string>({ default: '' })
const valueHtml = html // 语义别名,模板更直观
// ★ 编辑器实例用 shallowRef:实例内部结构复杂,不需要深度响应式(079 篇同款原则)
const editorRef = shallowRef()
const toolbarConfig = { excludeKeys: ['group-video'] } // 排除视频组(按需)
const editorConfig = { placeholder: '请输入内容...' }
const handleCreated = (editor: any) => {
editorRef.value = editor
}
// ★★★ 组件销毁前必须销毁编辑器实例,否则内存泄漏 + 切换路由报错
onBeforeUnmount(() => {
editorRef.value?.destroy()
})
</script>
<style scoped>
.rich-editor { border: 1px solid var(--el-border-color); z-index: 1; }
.toolbar { border-bottom: 1px solid var(--el-border-color); }
</style>
三个关键点:
- CSS 必须引入 (
@wangeditor/editor/dist/css/style.css),否则工具栏布局全乱 - 实例用 shallowRef(080 篇 ECharts 同款:重实例不做深度响应式)
- onBeforeUnmount 必调 destroy()------本篇头号大坑
三、使用:像普通表单一样绑 v-model
vue
<!-- src/views/ArticleEditView.vue -->
<template>
<el-input v-model="title" placeholder="文章标题" style="margin-bottom: 12px" />
<RichEditor v-model="content" />
<el-button type="primary" style="margin-top: 12px" @click="onSubmit">保存</el-button>
<!-- 预览:富文本是 HTML,展示用 v-html -->
<div class="preview" v-html="content"></div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import RichEditor from '@/components/RichEditor.vue'
const title = ref('')
const content = ref('<p>初始内容...</p>')
async function onSubmit() {
// 真实项目:await createArticle({ title: title.value, html: content.value })
console.log('提交 HTML:', content.value)
}
</script>
【截图位置:工具栏 + 编辑区输入"加粗文字和图片";下方预览区同步渲染】
四、图片上传配置
默认图片转 base64 塞进 HTML(文章一长提交体积爆炸),配置成上传服务器:
ts
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {
uploadImage: {
// ★ 上传接口(063 篇的 /api 前缀方案)
server: '/api/upload/image',
fieldName: 'file', // 后端接收的文件字段名
maxFileSize: 2 * 1024 * 1024, // 2MB 限制
// 后端返回格式约定:{ code: 0, data: { url: 'xxx' } } 时做映射
customInsert(res: any, insertFn: (url: string, alt?: string) => void) {
insertFn(res.data.url) // ★ 把图片 url 插进编辑器
}
}
}
}
后端约定:上传成功返回图片 URL,customInsert 负责把 URL 写进正文------对接任何后端格式都能适配。
五、踩坑记录
- 忘 destroy() :路由切换后控制台报错、内存泄漏------onBeforeUnmount 里
editorRef.value?.destroy(),本篇头号坑 - Vue3 装了 Vue2 的组件层 :
@wangeditor/editor-for-vue默认版本是 Vue2 组件------必须@next - 样式错乱/工具栏堆叠 :CSS 没引入------
import '@wangeditor/editor/dist/css/style.css' - 弹窗里 z-index 冲突 :工具栏下拉被 el-dialog 盖住------给编辑器容器加
z-index: 1(高于弹窗遮罩的层叠上下文),或弹窗append-to-body - 编辑器实例包 ref/reactive :深度代理内部复杂对象导致卡顿报错------shallowRef(080 篇同款)
- v-html 直接渲染用户内容:XSS 注入风险(085 篇 Checklist 有安全项)------展示端由后端过滤 XSS,或前端 DOMPurify 处理后再 v-html
- 回显内容丢失格式:后端存的 HTML 被转义过------确认存取都按原始 HTML 字符串,别擅自 encodeURIComponent
六、今日小结
- 组成:Toolbar + Editor 两件套 + 必引 CSS ;Vue3 装 @next 版组件层
- 数据流:v-model 绑 HTML 字符串,展示用 v-html(注意 XSS 过滤)
- 实例管理:shallowRef 存实例 + onBeforeUnmount destroy()(重实例三连的第三连:080 dispose、081 destroy)
- 图片上传:MENU_CONF.uploadImage + customInsert 适配任意后端
下篇预告
上传不止图片:头像、附件、大视频。下一篇 082 文件上传实战:el-upload 校验、自定义上传接 062 篇的 api,以及大文件分片上传的思路。