简介:面向需要在 Vue 项目中快速搭建富文本编辑场景的前端开发者,这份代码演示了如何组合 elementUI 与 froalaEditor,实现仿微信公众号后端的编辑界面,整体分为左侧文章管理与右侧内容编辑两大区域,结构清晰,适合作为后台管理系统的基础模板。压缩包仅 6KB,共 2 个 vue 文件,均为核心组件实现,便于直接阅读列表联动、编辑器渲染等关键交互逻辑;虽然包体小巧,但通过组件拆分展示了从文章选中到编辑区呈现的典型流程,可快速移植到实际项目。已有 4514 人学习,说明其贴合公众号式编辑需求的实用价值。按描述说明,资源需配合 vue-froala-wysiwyg、jquery 与 font-awesome 使用,安装依赖后即可对照调试验证。
1. 仿微信公众号文章编辑器:为什么我会把 Element UI 和 Froala Editor 搭在一起
很多内容管理后台接到“仿微信公众号编辑器”的需求时,第一反应是找一个大而全的富文本组件,结果不是工具栏塞满用不上的按钮,就是正文样式和微信公众平台后台完全两个味。我后来用得顺手的组合很固定:外层界面全部交给 Element UI,正文内核交给 Froala Editor,也就是标题里这套方案。它解决的问题很具体:让运营人员在系统里写文章时,所见即所得的体验接近微信后台,标题、正文、图片分离,发布前能核对字数与格式。
这套方案适合正在做内容管理后台、官网发布系统或内部编辑器的前端同学参考。Froala 编辑器自带排版功能已经接近微信公众号后台的常用操作,而 Element UI 负责把标题输入框、封面、发布按钮这些周边组件串成一个完整表单。下面从工程搭建开始,一步步把它跑通。
2. 工程搭建:Froala 进入 Vue 项目的最小步骤与选型取舍
2.1 选型对比:Froala、Quill、wangEditor 在“仿微信编辑器”场景里的差异
先说结论:在 Vue 2 + Element UI 的体系里,Froala 是最接近微信公众号编辑体验的商用编辑器内核,但它的授权方式是双许可,这一点必须在动手前确认清楚。我最早在一套内容系统里先试过 Quill,Quill 最大的问题是它的核心只处理纯文本格式,像字体颜色、背景色、引用块这些在微信公众号后台很常用的能力,要靠大量自定义 blot 去补,维护成本并不低。
wangEditor 在国产编辑器里体验也不错,但它对图片上传、粘贴内容清洗这些环节的控制粒度不如 Froala 细,尤其是当后端要求图片必须经过自己的存储服务时,Froala 的事件机制更好拦截和接管。表格对比一下四个方向:
| 编辑器 | 上手成本 | 仿微信排版还原度 | 图片上传接管 | 授权注意点 | 维护活跃度 |
|---|---|---|---|---|---|
| Froala Editor | 低 | 高 | 事件丰富,易接管 | GPL 或商业授权 | 高 |
| Quill | 中 | 中 | 需自写 Blot | 开源免费 | 中 |
| wangEditor | 低 | 中 | 需自写钩子 | 开源免费 | 中 |
| TinyMCE | 中 | 中 | 插件机制较复杂 | 核心开源,部分插件收费 | 高 |
如果你是内部运营系统,并且能接受 GPL 协议的条件,Froala 可以免费使用;如果做的是商业产品且不想开源自己的业务代码,就需要购买商业授权。这个判断不影响下面的代码写法,但影响上线合规,建议先跟法务确认。
2.2 安装与全局注册:两条命令让 Froala 联网可编辑
Froala 在 Vue 2 项目里通常通过两个包引入:编辑器本体froala-editor,以及 Vue 集成层vue-froala-editor。安装命令如下:
npm install froala-editor vue-froala-editor --save装完之后在入口文件里全局注册并引入样式和中文语言包:
// src/main.js import Vue from 'vue' import VueFroala from 'vue-froala-editor' // 编辑器主体样式,pkgd 表示打包好的完整样式 import 'froala-editor/css/froala_editor.pkgd.min.css' // 展示端样式:发布后渲染富文本内容也用这份样式 import 'froala-editor/css/froala_style.min.css' // 中文语言包,加载后可在 config 里指定 language import 'froala-editor/js/languages/zh_cn.js' Vue.use(VueFroala)这里说明两点:froala_editor.pkgd.min.css是编辑器工作时的编辑区样式,必须引入;froala_style.min.css是当你把编辑好的 HTML 拿到文章详情页渲染时也要用的样式,它规定了引用块、标题、表格在阅读场景下的默认外观。如果你只在编辑页引入,详情页展示时排版会明显“变味”,这一点先记住,后面避坑章还会提。
注册完成后,模板里就可以用<vue-froala>标签,v-model 绑定内容,config 绑定配置对象。注册名细节不关键,重要的是确认全局注册只做一次,不要在业务组件里反复注册。
2.3 最小配置跑通:一个空正文框先用起来
我用一个最简配置验证整条链路是否通:打开页面能看到编辑区、能打字、能实时把内容同步回 Vue 的数据对象。代码如下:
<template> <div class="editor-wrap"> <vue-froala v-model="content" :config="editorConfig" /> </div> </template> <script> export default { name: 'ArticleEditor', data() { return { content: '', editorConfig: { // 中文语言包在 main.js 已加载,这里指定语言 language: 'zh_cn', // 编辑区至少给到 500px,接近微信后台的写作区高度 heightMin: 500, placeholderText: '请输入正文内容', // 关闭自动保存,内容统一由 v-model 管理 autosave: false } } } } </script>这个最小配置跑通后,你会看到<vue-froala>自动渲染出一个带默认工具栏的编辑区。这里的v-model会同步 HTML 字符串,也就是说你不需要手动 getContent,Vue 数据里的content字段就是当前完整 HTML。单个field属性可以控制是显示工具栏还是只显示编辑区,如果只想看文本效果,临时在配置里加toolbarInline: false也可以。
3. 仿微信视觉核心:工具栏、字体与正文区的 Froala 参数配置
3.1 仿微信视觉第一步:理解微信公众号后台正文区的三层结构
微信公众号后台的编辑器视觉上其实只有三层:最外层是浅灰色的操作台页面背景,中间是白色正文卡片,最里面才是可以输入正文的内容区。很多仿写项目做出来不像,不是因为编辑器本身,而是没有把这三个层次还原。用 Froala 时,外层用 Element UI 的布局控制;中间白卡的宽度、圆角、阴影需要给编辑器的容器加一层样式;最里面的正文区宽度原则上和微信一样,固定在大约 677px。
我建议在封装编辑器组件时,把外层容器结构固定成“灰色页面底 + 白色卡片 + 内部正文区”,并把宽度限制写死在容器而不是编辑区。这样即使运营在窄屏笔记本上操作,正文区的换行也不会和最终发布效果产生明显差异。下面这段是容器结构的雏形:
<template> <div class="editor-page"> <div class="editor-card"> <vue-froala v-model="content" :config="editorConfig" /> </div> </div> </template> <style scoped> .editor-page { background: #f2f2f2; min-height: calc(100vh - 80px); padding: 20px; } .editor-card { max-width: 780px; margin: 0 auto; background: #fff; border-radius: 6px; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); padding: 30px 50px; } </style>注意editor-card的宽度我这里留到了 780px,内部正文区还要再做一次宽度约束,下一小节用 Froala 初始化参数完成。
3.2 工具栏按钮分组:缩小到微信后台常用的 16 个按钮
Froala 默认工具栏按钮非常多,包括视频、表格、全屏、HTML 源码等。微信公众号后台的工具栏其实很克制,保留的无非是加粗、斜体、下划线、字体、字号、颜色、对齐、引用、有序无序列表、插入链接、插入图片、分割线这几组。我一般会把toolbarButtons按分组方式重写,而不是用toolbarButtons传一个平铺数组,分组的好处是移动端能自动折叠成多级菜单。
editorConfig: { language: 'zh_cn', heightMin: 500, placeholderText: '请输入正文内容', toolbarButtons: { moreText: { buttons: [ 'bold', 'italic', 'underline', 'strikeThrough', 'fontFamily', 'fontSize', 'textColor', 'backgroundColor' ], buttonsVisible: 8 }, moreParagraph: { buttons: [ 'alignLeft', 'alignCenter', 'alignRight', 'alignJustify', 'formatOL', 'formatUL', 'quote', 'insertHR' ], buttonsVisible: 4 }, moreRich: { buttons: ['insertLink', 'insertImage'], buttonsVisible: 2 }, moreMisc: { buttons: ['undo', 'redo'], buttonsVisible: 2 } } }buttonsVisible控制的是这个分组在工具栏上默认展开几个按钮,超过的部分收进右侧折叠菜单。微信公众号后台实际常用按钮不超过 16 个,我这样分组后 PC 端一眼能看全,不会让运营人员产生“工具太多反而找不到”的困扰。
这里要说明一个取舍:原文中我把视频、表格、代码块都移除了。如果业务确实需要表格,可以在moreRich里加回insertTable;视频不建议加,因为视频的存储和转码通常不在编辑系统能力范围内,后续发布展示时经常翻车,宁可让运营先传视频封面和链接,用insertLink代替。
3.3 字体与字号参数:把中文字体灌进 Froala 的下拉面板
Froala 默认的fontFamily下拉面板里全是西文字体,微软雅黑、宋体、黑体一个都找不到。这是很多项目仿微信后台时被吐槽最多的地方。解决方法是初始化时用fontFamily配置项覆盖字体下拉列表的数据源:
fontFamily: { '微软雅黑': 'Microsoft YaHei, sans-serif', '宋体': 'SimSun, serif', '黑体': 'SimHei, sans-serif', '思源黑体': '"Source Han Sans SC", "Noto Sans SC", sans-serif', 'Arial': 'Arial, Helvetica, sans-serif', 'Georgia': 'Georgia, Times New Roman, serif' }, fontFamilySelection: true配置里 object 的 key 是下拉框显示的名称,value 是实际写入 CSS 的 font-family 值。这里有个经验:value 里必须带上备选字体,比如宋体和思源黑体要跟在 serif/sans-serif 后面,否则某些 Linux 服务器上字体渲染异常时,正文会直接掉成默认字体。fontFamilySelection: true让选中字体后实时更新内容区。
字号方面,微信公众号后台正文一般默认 16px,标题可以到 20px 以上。Froala 的fontSizeDefaultSelection可以设置默认选中字号,我在项目里会配成:
fontSize: ['10', '12', '14', '16', '18', '20', '24', '30'], fontSizeDefaultSelection: '16'注意fontSize传的是字符串数组,不要传数字,否则部分版本初始化时会回退成默认列表。
3.4 正文区样式覆盖:677px 宽度、默认字号与引用缩进
Froala 初始化后,编辑区会被包在.fr-wrapper .fr-element这个结构里。微信公众号后台的正文区宽度不是全宽,而是固定约 677px,左右留白。我在项目里做了两层约束:编辑器外层不做宽度限制,编辑区内用 CSS 覆盖:
.editor-card .fr-wrapper .fr-element { width: 677px; margin: 0 auto; font-size: 16px; line-height: 1.75; color: #3f3f3f; word-break: break-word; } .editor-card .fr-element blockquote { margin: 16px 0; padding: 8px 16px; border-left: 4px solid #d9d9d9; background: #fafafa; color: #6c6c6c; } .editor-card .fr-element img { max-width: 100%; height: auto; }这样正文区在任何屏幕宽度下都保持 677px,和微信公众号后台阅读器的正文宽度一致。blockquote的样式没有写在 Froala 配置里,直接走 CSS 覆盖,发布端展示页也复用同一份样式文件,保证编辑和阅读效果一致。
另外有个细节:Froala 会把图片默认渲染成带边框和操作按钮的模式,在仿微信场景下图片不需要圆角上的控制条。可以用配置项关掉不必要的东西:
imageEditButtons: ['imageReplace', 'imageRemove'], imageResize: false, imageRoundPercent: 04. Element UI 联动:标题栏、底部操作栏与自定义图片上传
4.1 页面骨架:用 el-input / el-button 搭出标题区和发布栏
Froala 只负责正文内容,文章标题、发布按钮、封面图这些必须由 Element UI 组件搭出来。我通常会做一个发布面板,固定在页面底部,标题输入放在正文卡片上方,整体结构如下:
<template> <div class="editor-page"> <div class="editor-card"> <div class="article-title"> <el-input v-model="title" maxlength="64" show-word-limit placeholder="请输入文章标题" /> </div> <vue-froala v-model="content" :config="editorConfig" /> </div> <div class="editor-footer"> <el-button @click="onPreview">预览</el-button> <el-button type="primary" @click="onPublish">发布</el-button> </div> </div> </template>标题输入框单独放在正文上方,而不是和正文混在同一个富文本里,这样运营不会误把标题写进正文内容。el-input的maxlength直接限制标题长度,微信公众号标题最长 64 字,这个限制是硬性的,不需要后端再校验一遍。
底部操作栏用position: fixed固定在窗口底部,在左右两侧分别放“保存草稿”和“发布”按钮。发布前需要做两件事:校验标题非空、正文非空,以及把正文里的图片链接收回来做一次域名校验,后面小节会展开讲。
4.2 自定义图片上传:拦截 Froala 默认上传并切换到 el-upload 弹窗
Froala 默认的图片上传走imageUploadURL,后端必须返回固定结构的 JSON,格式大致是{ "link": "https://..." },否则编辑器无法识别。很多团队卡在这一步:后端返回的是自己业务的{ code, data }结构,Froala 不认识,图片就没法插入。项目里我一般会绕开 Froala 的默认上传,用image.beforeUpload事件拦截,然后弹一个 Element UI 的el-dialog加el-upload:
<template> <div> <vue-froala ref="froalaEditor" v-model="content" :config="editorConfig" /> <el-dialog :visible.sync="uploadVisible" title="上传图片" width="480px" > <el-upload :action="uploadAction" :headers="uploadHeaders" :before-upload="beforeUpload" :on-success="onUploadSuccess" :on-error="onUploadError" :limit="1" list-type="picture-card" > <i class="el-icon-plus" /> </el-upload> </el-dialog> </div> </template>拦截的逻辑要写在 Froala 的events里,并且注意事件的回调用self引用 Vue 实例:
const self = this editorConfig: { events: { 'image.beforeUpload': function() { // 阻止 Froala 默认的图片上传流程 self.uploadVisible = true return false } } }return false是关键,它告诉 Froala “我接管了上传”。弹窗出来后,运营选完图,el-upload用自己的接口上传,成功后拿到图片 URL,然后调用 Froala 的 API 把图片插入正文光标处:
onUploadSuccess(response) { // 假设后端返回 { data: { url } } const imgUrl = response.data.url this.uploadVisible = false const editor = this.$refs.froalaEditor.editor if (editor) { editor.image.insert(imgUrl, false, null, null, function() { // 插入成功后清空上传组件,避免第二次打开还残留上一张图 this.uploadFiles = [] }) } }editor.image.insert这行参数含义分别是:图片地址、是否做 XSS 过滤、附加属性、附加样式、插入成功回调。这里this在回调里指向 Froala 实例,不是 Vue 组件,所以重置上传列表要再用一层普通函数或者箭头函数。这个改法绕开了 Froala 对后端返回格式的要求,同时也便于接入公司自己已有的上传组件,是最常见的落地做法。
4.3 内容读取与字数校验:拿回 HTML 并计算正文文本长度
发布前运营通常需要知道正文写了多少字,微信公众号后台也有字数统计。HTML 字符串不能直接数长度,要把标签、图片注释、空白全部剥离。我在发布面板上放一个字数提示,通过 Froala 的contentChanged事件实时更新:
const self = this editorConfig: { events: { 'contentChanged': function() { self.contentTextCount = this.el.textContent.trim().length } } }this.el是编辑器的根 DOM 节点,textContent会去掉所有 HTML 标签,得到纯文本。这里有个坑:textContent会把图片的 alt 文本也统计进去,如果运营给图片加了很多描述文字,字数会有偏差。我的处理是发布前再调一次正文解析方法,主动剔除图片占位符:
contentTextCount: function() { const plainText = this.content .replace(/<img[^>]*>/g, '') .replace(/<[^>]+>/g, '') .replace(/ /g, ' ') .trim() this.textCount = plainText.length }发布按钮的校验可以统一放在onPublish里,同时满足“标题非空、正文非空、正文不超过一定字数”三个条件才允许提交。字数上限一般配合后端接口的存储设计,常见的是 2 万字,够微信公众号长文使用。
5. 常见问题与避坑排查:图片上传、授权弹窗与样式丢失的 5 条踩坑记录
5.1 图片上传成功了,编辑器里却只显示一个断裂图标
现象:Froala 配置imageUploadURL后,后端明明收到了文件并返回了 URL,但编辑器里图片位置是一个破裂的图片图标。
原因:Froala 对后端返回结构有固定要求,字段名必须是link,如果后端返回的是{ code: 200, data: { url: '...' } },Froala 取不到link,自然插不进去。
解决:让后端按 Froala 规约返回{ "link": "https://..." },或者像第 4 章那样用image.beforeUpload拦截,走自己的上传组件和返回结构。如果你希望保留 Froala 默认上传链路,至少要保证返回的 JSON 里有一层link字段。
5.2 字体下拉里找不到微软雅黑和宋体
现象:点开字体下拉列表,只有 Arial、Georgia 等西文字体,整个项目里所有中文字体选项都不存在。
原因:Froala 初始化时内置的fontFamily列表只包含西文 web safe 字体,中文字体需要手动配置才会出现在面板里。
解决:在初始化配置里显式传入fontFamily对象,key 是下拉显示名称,value 是 CSS 字体栈。如果配置了还是看不到,检查是否在你的editorConfig被后续代码覆盖,或者语言包加载失败导致配置项没生效。配置后记得启用fontFamilySelection: true,否则选中下拉值不会立即作用到选区。
5.3 控制台出现 “Unlicensed” 或编辑器页脚多出水印
现象:编辑器初始化后,控制台有关于未授权的提示,编辑区页脚出现了 “Unlicensed” 字样的水印。
原因:Froala 是 GPL 与商业双授权产品,未按协议购买商业授权,且应用不满足开源条件时,会在界面上给出提示。
解决:内部非商业项目,确认整个项目满足 GPL 协议要求后,可以在初始化配置里显式声明licenseKey: 'GPL'来消掉水印提示;商业项目走公司采购流程,获取正式授权码并配置到licenseKey。这不是编辑器 bug,不要试图通过 CSS 隐藏水印,授权合规问题会留在代码审计里。
5.4 v-model 绑定不上:编辑器初始化完成前就赋值了
现象:弹窗里打开编辑器时,v-model绑定的 HTML 没有渲染进编辑区,或者第二次打开时内容还是旧的。
原因:<vue-froala>的初始化是异步的,弹窗打开瞬间把旧内容赋给content,此时 Froala 内部的文档模型还没准备好,赋值被忽略。另一方面,弹窗关闭时组件没有被真正销毁,重新打开时保留了上一次的内容。
解决:弹窗场景下用v-if控制编辑器挂载,打开弹窗后再渲染<vue-froala>,并且通过@init事件确认初始化完成后再回填内容:
<el-dialog :visible.sync="dialogVisible"> <vue-froala v-if="dialogVisible" v-model="content" :config="editorConfig" @init="onEditorInit" /> </el-dialog>onEditorInit里针对特殊业务做数据回填,比如从草稿箱载入 HTML 时,需要等init完成后再操作。用v-if也可以避免 keep-alive 缓存引起的实例复用问题。
5.5 正文区的引用、代码块样式在项目全局 reset 下全部变形
现象:编辑状态下引用块有灰色底和左侧竖线,发布到详情页后引用块样式消失,或者正文图片被打上项目全局img { max-width: 100% }以外的边框,排版明显错位。
原因:详情页没有引入froala_style.min.css,编辑器渲染时依赖这套基础样式;另一个常见情况是项目全局样式里写了blockquote, pre { margin: 0; padding: 0; }这类 reset,把 Froala 的内部样式冲掉了。
解决:详情展示页同样引入froala_style.min.css,或者在全局样式里对.fr-view作用域下的blockquote、pre、table、img做保护性重置。我自己的做法是展示端不直接用全局 reset,而是单独做一层article-content样式,把 Froala 的排版规则复制过来,这样样式属于展示页自己,与后台版本无关。
6. 进阶细节:图片域名校验、正文模板和快捷键习惯
6.1 发布前批量校验正文图片域名
编辑内容后,运营可能从外部网站复制了带外链图片的文本,这类图片在发布后容易因防盗链失效。我通常在onPublish里先收集正文中所有img的src,再过滤掉非允许域名的链接:
getExternalImages(html) { const regex = /<img[^>]+src="([^">]+)"/g const urls = [] let match while ((match = regex.exec(html)) !== null) { urls.push(match[1]) } return urls.filter(url => !this.allowedImageDomain.some(d => url.startsWith(d))) }如果发现外链,发布前弹确认框,提醒运营替换成本地上传图片。这个校验写在组件里,不依赖后端,成本很低,却能避免掉大部分线上图片裂图问题。
6.2 把常用排版做成可复用模板
公众号运营的文章里,引用、小标题、分割线使用频率很高。我会在工具栏右侧单独放一个 Element UI 的“模板”按钮,点击后把预设的 HTML 片段通过editor.html.insertHTML插入光标处:
insertTemplate(type) { const editor = this.$refs.froalaEditor.editor if (type === 'section-title') { editor.html.insertHTML('<p style="font-size:20px;font-weight:bold;margin:24px 0 12px;">章节标题</p>') } else if (type === 'quote') { editor.html.insertHTML('<blockquote>这里是引用内容</blockquote>') } }模板集中在配置文件里管理,运营新增模板不需要改编辑器本身,只改模板列表就行。
6.3 顺手绑定两个高频快捷键
Froala 默认支持 Ctrl+B 加粗,但运营最常用“插入分割线”却默认没有快捷键。可以在init后通过编辑器事件接口补两个习惯性操作:
onEditorInit(editor) { const self = this editor.events.on('keydown', (e) => { if (e.keyCode === 13 && !e.shiftKey && self.isInEmptyParagraph()) { editor.html.insert('<hr>') } }) }这里的isInEmptyParagraph是我自己写的一个判断函数,检测光标是否位于空段落。做成这样之后,运营写稿时按回车就能自动生成分割线,体验更接近微信后台。每个项目最终都会沉淀一套属于自己的细节改法,我现在接到编辑器需求,都会先问一句“运营最常用的三个操作是什么”,而不是直接去铺一堆功能按钮。希望帮到你。
本文还有配套的精品资源,点击获取