资讯动态

Vue项目中TinyMCE源码模式:官方code插件与自定义弹窗完整实践

发布时间:2026/10/14 1:51:05 来源:尧图企业网站定制
在Vue项目里跟富文本编辑器打交道TinyMCE基本是我第一个会去尝试的方案。插件丰富、浏览器兼容性好、API也稳定社区里遇到问题基本都能搜到答案。但几乎每个后台项目都会提一个需求在工具栏上放一个“源码”按钮让有权限的人直接查看和编辑HTML。默认配置下这个入口是藏起来的不加插件不写代码光靠普通配置调不出来。这篇文章就把我实际接入过程中用到的两种方案完整写出来一是用官方code插件快速搞定二是不依赖插件、自己用Vue组件实现源码弹窗。顺手把安全过滤、v-model同步、资源加载这几个坑也一起讲清楚做过类似功能的朋友应该能少走点弯路。1. 先把概念理清官方code插件和自定义源码模式的区别1.1 为什么默认工具栏没有源码按钮TinyMCE 的架构是“核心编辑器 插件”。官方发布包里带了大量插件文件但默认初始化时只加载你声明的那几个插件。源码编辑相关的插件叫code它的作用是弹出一个对话框里面是当前编辑器内容的 HTML 源码。你如果不把code加进plugins工具栏里自然就不会有对应的code按钮。我接触过不少刚上手的开发者一看到“源码模式”就去找codesample。注意codesample是代码示例插件负责插入一段带语法高亮的代码块不是编辑整篇HTML源码。这两者经常被搞混。假如你的需求只是让用户粘贴一段代码进文章里展示应该用codesample假如需求是“看到整个内容的HTML并且可以改”才是code插件的事。还有一个容易忽视的点TinyMCE 每个大版本的插件注册API略有不同。我主要使用的是 5.x 和 6.x这两个版本里code插件都是内置的不需要额外下载。你只需要关心你的自托管资源路径是否正确。如果你用的是打包器直接 import 某些插件要确认插件文件确实随构建产物一起输出了。1.2 官方插件和自研弹窗的取舍官方code插件的好处是零成本声明插件、加按钮、点开就是编辑框改完点确定内容直接更新。坏处也很明显那个编辑框就是一个最朴素的 textarea没有行号、没有高亮、没有自动格式化也没有办法把弹窗里的数据和业务逻辑打通。自制弹窗最大的价值不是“因为官方插件不好”而是当你需要把源码编辑做进业务流程时官方插件给不了编程接口。举例来说我希望源码弹窗里同时显示字符数统计、标签闭合检查、一键格式化甚至把源码编辑框替换成另一个编辑器。这些需求官方code插件完全做不到只能自己写。所以我的建议是如果你只是需要一个“能改源码”的入口直接上code插件如果你要把它变成后台系统的一个专业功能模块那自制弹窗是更正确的路。后面我会把两条路线都写一遍你可以根据项目情况选。2. 官方code插件快速接入源码模式2.1 自托管TinyMCE的资源布局实际开发里最常用的不是云服务方式而是把 TinyMCE 的静态资源放在自己项目里尤其内网部署很常见。第一步先安装两个包npm install tinymce tinymce/tinymce-vue注意新版tinymce/tinymce-vue支持 Vue 3Vue 2 要用老版本。装完之后node_modules/tinymce下有langs、plugins、skins、themes几个目录必须原封不动发布到前端静态目录。如果是 Vite 项目我一般放在public/tinymce下然后初始化时指定base_url: /tinymce。手动复制可以但团队协作时容易漏。比如有人重新npm install之后忘了拷贝本地跑得通部署到服务器就 404。建议用vite-plugin-static-copy在构建时自动同步import { viteStaticCopy } from vite-plugin-static-copy; export default defineConfig({ plugins: [ vue(), viteStaticCopy({ targets: [ { src: node_modules/tinymce/**/*, dest: tinymce } ] }) ] });这样构建产物里会自动带上完整资源不会再出现“本地正常、线上白屏”的问题。如果你用的是 webpack也可以用copy-webpack-plugin做同样的事。核心思路就是让静态资源同步成为构建的一部分而不是靠手工。2.2 在Vue组件里配置code插件下面是最小可用的 Vue 3 组件。我习惯把编辑器封装成src/components/TinyEditor.vue对外暴露v-model。template editor v-modelcontent api-keyno-api-key :initinitOptions / /template script setup import Editor from tinymce/tinymce-vue; import { computed } from vue; const props defineProps({ modelValue: { type: String, default: } }); const emit defineEmits([update:modelValue]); const content computed({ get: () props.modelValue, set: (val) emit(update:modelValue, val) }); const initOptions { base_url: /tinymce, suffix: .min, language: zh_CN, height: 500, plugins: code, toolbar: code, branding: false }; /script这里要注意plugins和toolbar里的code要同时配置。plugins决定编辑器加载这个能力toolbar决定把按钮放到哪条工具栏。只写plugins不写toolbar功能虽然加载了但界面上没有任何入口。反过来只写toolbar不写plugins初始化时会报控制台警告按钮也不会有反应。如果你担心工具栏太挤可以把源码按钮放到菜单栏。比如在initOptions里加menubar: tools然后toolbar: code就不写了。但大多数用户习惯从工具栏找入口我更推荐直接在工具栏放一个code按钮。2.3 源码弹窗里的编辑体验和编码细节点击code按钮后弹窗里是一个普通 textarea显示的是editor.getContent()返回的 HTML。改完点 OKTinyMCE 内部执行editor.setContent(value)。这里有个很影响体验的点TinyMCE 默认会把内容里的一些字符做实体编码比如会变成amp;。如果你在源码模式下看到一堆amp;可能不是内容真的错了而是实体编码导致的。我的经验是在initOptions里加上entity_encoding: raw,这样非特殊字符不会被强制转义。但要注意这会让特殊字符以原始形态保存。如果你的后端服务端存储时还会再做一层htmlspecialchars之类的转义就会重复编码导致页面显示成amp;amp;。碰到这种情况要么前后端约好只转义一次要么在读取时统一处理。如果业务里需要经常从 Word 粘贴内容源码模式下会看到大量!-- [if !supportLists] --之类的注释和o:p标签。这个时候可以配合valid_elements配置来清理但不要期望源码模式本身帮你优化 HTML。它不是格式化工具只是给有经验的人一个直接操作的空间。3. 自制源码模式弹窗不依赖官方插件3.1 整体设计编辑器实例交给Vue管理自制方案的第一步是拿到 editor 实例。用tinymce/tinymce-vue时可以通过onInithandleInit拿到editor :initinitOptions onInithandleInit /保存到let editorRef null。自定义按钮在initOptions.setup中注册也可以在初始化后通过editor.ui.registry.addButton注册。我更推荐setup因为它在编辑器初始化的早期同步执行能够保证工具栏渲染时按钮已经注册成功。3.2 注册自定义按钮的完整代码我要注册的按钮叫sourcecode图标直接沿用编辑器内置的sourcecode图标tooltip 写“源码编辑”。点击后打开一个由 Vue 控制的弹窗。let editorRef null; function handleInit(_evt, editor) { editorRef editor; } const initOptions { plugins: lists link image, toolbar: blocks | sourcecode, setup(editor) { editor.ui.registry.addButton(sourcecode, { icon: sourcecode, tooltip: 源码编辑, onAction() { openSourceDialog(editor); } }); } };注意addButton必须在setup里调用不能用setTimeout等初始化完成后再去 add因为工具栏渲染发生在初始化阶段注册太晚会看不到按钮。另外如果你的工具栏里同时想保留撤销重做可以写成toolbar: undo redo | blocks | sourcecode。3.3 弹窗组件里的数据同步细节弹窗我直接用 Vue 的普通组件模拟不引入第三方弹窗库减少依赖。核心代码如下template div v-ifvisible classsource-mask div classsource-dialog div classdialog-header spanHTML源码/span button clickcancel关闭/button /div textarea v-modelsourceCode classsource-textarea/textarea div classdialog-footer span classline-count{{ lineCount }}行/span button clickformat格式化/button button clickconfirm确定/button /div /div /div /template script setup import { ref, computed } from vue; const visible ref(false); const sourceCode ref(); let targetEditor null; function open(editor) { targetEditor editor; sourceCode.value editor.getContent(); visible.value true; } const lineCount computed(() sourceCode.value.split(\n).length); function cancel() { visible.value false; } function confirm() { targetEditor.setContent(sourceCode.value); targetEditor.fire(change); visible.value false; } function format() { // 这里可以接入prettier做HTML格式化 } defineExpose({ open }); /script打开时执行editor.getContent()确定时执行editor.setContent(sourceCode.value)然后editor.fire(change)。这个fire(change)很关键。如果你只setContent不 fire某些 v-model 封装或表单提交逻辑不会及时感知内容变化导致编辑器内容变了、页面状态没变或者保存出来还是旧内容。还有一个细节弹窗里 textarea 的换行不要转换。textarea 本身就是按真实换行存储的不要让 HTML 格式化把你的换行替换成空段落。业务里如果发现弹窗里明明写了很多空行保存后空行消失了多半是编辑器设置里forced_root_block导致的后面会细说。3.4 在自制弹窗里集成格式化、实时预览和危险代码提示如果只是把getContent()结果塞给 textarea很多人会认为“这跟官方插件有什么差别”。所以自制方案里我更推荐顺手做增强。第一个是 HTML 格式化。浏览器端可以引入prettier然后调用import { format } from prettier/standalone; import htmlParser from prettier/plugins/html; async function formatHtml() { sourceCode.value await format(sourceCode.value, { parser: html, plugins: [htmlParser], printWidth: 80, tabWidth: 2 }); }这样源码弹窗里就能看到整齐的缩进比官方 textarea 好用不少。第二个是实时预览。可以给弹窗加一个 iframe用srcdoc绑定当前的源码iframe classpreview-frame :srcdocsourceCode/iframe这样改一行代码右侧预览立刻刷新对调样式的人非常友好。注意 iframe 里的脚本执行问题预览只面向可信用户开放否则等于给 XSS 开了一扇门。第三个是危险代码提示。不引入复杂解析器的话可以做一个简单校验把字符串里的onerror、script、iframe等关键词找出来在确定时给出警告。但纯正则不可靠只能提示不能拦截。真正可信的过滤必须放在服务端前端校验只是提升体验。后面我会单独展开安全部分。4. 源码模式惹出来的安全和数据同步问题4.1 允许源码编辑等于允许富文本之外的注入风险这一点必须放在前面开启源码模式后用户可以直接往 HTML 里写script、iframe、img onerror...。如果是内部管理系统用户是可信任的运营人员风险还可控如果这个编辑器面对互联网用户我会在工具栏上把源码按钮收起来或者至少做到服务端使用白名单过滤只允许p、span、img、a、ul、ol、li、blockquote、pre等常用标签禁止script、iframe、object、embed、link以及所有on*事件属性如果一定要允许style用 CSS 解析器而不是简单正则。前端可以用 DOMPurify 做一个即时预览前的过滤但不要指望它能替代服务端。我在某个后台项目里看到过用户通过源码模式把整段脚本塞进文章最终在他人浏览器里执行的情况。这不是 TinyMCE 的问题是业务没有做好权限隔离。4.2 v-model、editor.save()和表单提交TinyMCE 在传统表单里会把内容同步到原来的 textarea。但你如果用 Vue 的v-model双向绑定组件内部的隐藏 textarea 其实被框架托管了表单直接提交时不一定能拿到最新内容。常见的做法是在组件内监听input或change事件update:modelValue提交时如果担心脏数据先调用editor.save()强制把编辑器内容写回原 textarea再读取。我自己封装的代码会在保存前执行function handleSubmit() { editorRef?.save(); const html editorRef.getContent(); // 继续提交逻辑 }这样能规避“预览显示正常提交内容为空”的灵异问题。尤其是页面里同时存在多个编辑器或者编辑器在弹窗里初始化时这个步骤更容易踩坑。另外如果你的弹窗关闭时会销毁编辑器组件提交前一定要确保change事件已经触发否则最后几笔修改可能丢失。4.3 特殊字符、相对路径和源码编辑显示源码模式下最容易被吐槽的就是实体编码和相对路径。图片如果用相对路径/uploads/1.jpg在源码里看到没问题如果后台部署在子目录路径可能变成/sub/uploads/1.jpg。这需要你在初始化里配置base_url和document_base_url来统一资源地址不是源码模式本身的问题。还有一个容易被忽略的点getContent()返回的内容默认是 XHTML 风格空标签可能会被写成br /或者img src... /。在源码模式下看到这些自闭合标签不必惊慌只要不要在后端用正则去匹配br就能正常显示。如果你希望保存的内容保持 HTML5 风格可以设置xhtml: false这样输出更接近手写 HTML。但要注意如果你用了比较老的 TinyMCE 4这个配置的兼容性并不好。5. 常见问题排查与避坑记录5.1 点击源码按钮没有反应或控制台报错我把这类问题整理成了排查顺序表现象大概率原因处理方式按钮不存在plugins/toolbar没配置code检查 initOptions 是否同时包含plugins: code和toolbar: code按钮存在但弹窗空白skins或themes资源没加载在Network里看 tinymce 目录资源是否404重新复制静态资源初始化报“Missing plugin”插件文件路径错误确认base_url自托管时插件目录是否在/tinymce/plugins/code/plugin.min.js点击无响应且控制台无错误浏览器缓存了旧版本强刷并检查TinyMCE版本升级后插件文件名后缀可能变化弹窗能打开但内容为空编辑器初始化未完成就调用getContent确认在onInit之后再打开弹窗保存后内容和弹窗不一致v-model覆盖在 confirm 里手动 emit避免使用不完整的双向绑定这个表适合大多数接入场景。尤其是“本地开发正常、部署到服务器后按钮不见了”基本都是资源同步问题检查构建配置比检查代码更快。5.2 修改源码后编辑器内容没有变化先确认是不是setContent被后续逻辑覆盖了。在自制弹窗里我踩过的一个坑是打开源码弹窗时保存了 editorRef但在弹窗组件里又用 props 渲染了一份源码确定时setContent后 props 数据马上被父组件回传把新内容又覆盖成了旧内容。解决办法是确定后先emit(update:modelValue, newHtml)再让父组件更新数据不要依赖编辑器内部事件去改 props 对应变量。如果是官方code插件大多是因为你用了content_style或者自定义样式把某些元素隐藏了看起来“没变化”。此时打开浏览器的元素检查看看 DOM 节点是否真的更新。如果确实更新了那就是样式问题如果没更新才是数据同步问题。5.3 源码模式中空行、换行被吞掉TinyMCE 默认的forced_root_block为p它会自动给内容补 p 标签。源码模式下你写div第一行/div div第二行/div保存后 TinyMCE 可能给你改成p第一行/p p第二行/p这不是 bug是编辑器在尝试生成规范的结构。如果你希望保留更多原始结构设置forced_root_block: ,但这样会让纯文本粘贴时没有段落包裹后续样式可能乱。一般我保留默认p业务上用样式控制间距。如果你在源码弹窗里看到空行消失先排查这个问题不要以为是自己 setContent 写错了。5.4 给外部按钮添加打开源码弹窗的入口很多后台设计里“源码”不在工具栏而是放在页面右上角的“更多操作”里。这时要暴露一个方法。在封装的TinyEditor.vue里defineExpose({ openSource: () { if (editorRef) { // 打开自制的源码弹窗 } } });父组件通过 ref 调用即可。这个方法在官方code插件下不太好做因为官方没有公开打开弹窗的 API。自制方案在这类自定义交互上具备天然优势。如果你不想完全自制也可以把官方按钮藏起来然后自己模拟点击但那样比直接自制还麻烦不推荐。6. 权限控制与功能开关的落地思路源码编辑这种能力最适合的落地方式不是“所有人可用”而是“按角色渲染”。我在实际项目里通常会在工具栏配置上做一个动态拼接function buildToolbar(role) { if (role admin) { return blocks | sourcecode | undo redo | link image; } return blocks | undo redo | link image; }这样普通用户界面上根本看不到源码按钮管理员登录后才出现。把这个逻辑放在一个配置函数里要比在模板里写一堆v-if干净得多。权限控制之外还可以做一个操作审计当源码内容被修改时把新旧内容都记录到日志里。因为源码模式能改的东西太底层一旦出现问题追踪成本很高。日志不一定要展示但保存下来对排查线上问题很有帮助。这块不属于前端核心但属于“上了生产环境之后会被感谢”的设计。最后分享一点体验我在实际项目里最终采用的方式是普通编辑框用官方插件集源码按钮单独做成自制弹窗并且只在“管理员”身份下渲染。理由很简单普通用户基本用不到源码却容易把布局改坏管理员希望自由修改模板片段哪怕多一个格式化按钮都很有价值。代码量不多但对后台上手体验的提升非常明显。如果你也要做类似功能建议先把官方code插件跑通作为兜底再评估自制弹窗的收益不要一上来就重复造轮子。你现在看到这个功能实现后最值得花心思的其实是权限控制和内容安全这两个点想清楚了剩下的都是体力活。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑