简介:TinyMCE 编辑器 v6.2.0 是一款面向 Web 开发者与计算机专业学生的开源富文本编辑器源码包,能够嵌入 CMS、论坛、博客或毕业设计项目,提供所见即所得的网页文字编辑能力。压缩包共含 64 个文件,其中 37 个 js 文件承载编辑器核心逻辑与插件接口,22 个 css 文件定义界面主题与响应式布局,另附 md 文档、ts 类型定义、txt 与 htm 格式的说明文件,整体体积仅 724KB,集成门槛低。目前已有 197 人学习下载。该版本延续 TinyMCE 的跨浏览器兼容、国际化、API 事件与插件系统等特性,源码开放,既适合开发者研究其内部机制并按需定制功能,也可作为 JavaScript 库典型教学案例。压缩包内还包含版本说明与更新日志,配合完整目录结构,读者可快速定位核心文件、对照升级记录,在论文实现或商业项目中直接复用。
1. TinyMCE v6.2.0:富文本编辑器这个坑,这份资源能帮你一步填平
做后台管理系统、毕业设计论文里的内容发布模块时,几乎都会卡在同一件事上:textarea 只能填纯文本,用户要的是能加粗、插表格、贴代码块的编辑区。TinyMCE v6.2.0 就是一个开源富文本编辑器源码包,它把一段隐藏的 textarea 变成带工具栏的可视化编辑区域,图文混排、表格、代码高亮、图片上传都能在页面里直接完成。对正在整合源码、做毕设功能演示、或在业务项目里需要快速落地的开发者来说,这份资源的价值是把“编辑器从哪来、怎么初始化、怎么取值、踩过哪些坑”这条链路一次性讲清楚,省掉自己从零造轮子的排查时间。
2. 先弄懂运作机制:初始化流程、配置项与内容流转
2.1 初始化时,TinyMCE 在页面里到底做了什么
很多人以为 tinymce.init 只是“把 textarea 变好看一点”,实际上它执行了一整套替换流程:扫描 DOM 找出 selector 匹配的元素,隐藏原始 textarea 或 div,创建同尺寸的 iframe,在 iframe 内部注入编辑器文档、皮肤样式和交互脚本,再把原 textarea 的 value 作为初始内容填进去,最后挂载键盘、鼠标、粘贴事件。整个过程对业务代码是透明的,但如果你不知道它替换了原始元素,后面取值、清空、复用时就会出现各种玄学问题。
tinymce.init({ selector: '#editorArea', height: 420, plugins: 'lists link table', toolbar: 'undo redo | bold italic | bullist numlist | table', init_instance_callback: (editor) => { console.log('初始化完成,实例 id 为', editor.id); } });逻辑说明:这段代码是日常项目里最基础的初始化形态。init 是 TinyMCE 的唯一入口,内部完成 DOM 替换和 iframe 构建;init_instance_callback 在编辑器实例真正可用时回调,比直接写后面的代码更安全,适合在这里做初始数据填充。参数说明里,selector 建议用唯一 id,用 class 会一次性初始化多个元素;height 设置编辑区最小高度;plugins 和 toolbar 决定功能范围,也直接决定引入的资源体积。
2.2 配置项拆开读:哪些参数值得调
TinyMCE 的配置项有几十个,实际开发中高频改动的其实就固定那几项。下面这张表按“影响视觉 / 影响功能 / 影响体积”做了分类,方便你接手项目时快速定位问题。
| 配置项 | 作用 | 常用取值 | 注意点 |
|---|---|---|---|
| selector | 指定要替换的元素 | #editorArea | 唯一 id,别用 class |
| height / min_height | 编辑区高度 | 300 ~ 500 | 数字单位是像素 |
| menubar | 顶部菜单栏 | 'edit insert format' | 不需要就设 false |
| plugins | 功能插件列表 | 'lists link image table' | 空格分隔,按需加载 |
| toolbar | 工具栏按钮组合 | 'undo redo | bold italic' | 竖线是分组分隔符 |
| language | 界面语言 | 'zh_CN' | 需要额外语言包 |
| promotion / branding | 右下角官方提示 | false | v6 里对应 promotion |
| content_style | 编辑区默认样式 | 'body { font-family: sans-serif; }' | 影响编辑时看到的排版 |
| skin | 皮肤主题 | 'oxide'默认 | 换了皮肤要同步测试 |
参数说明里最容易被忽略的是 plugins 和 toolbar 的对应关系:插件不加载,toolbar 写了对应按钮也不会显示;反过来,toolbar 没写按钮,插件加载了也不会有入口。改配置后如果界面没变化,优先检查这两个地方是否配套。
2.3 内容怎么进、怎么出:getContent 与 setContent 的边界
编辑器接管 textarea 之后,原来的 textarea.value 就不再实时同步了。取值必须走编辑器 API,赋值也一样。getContent 返回的是 HTML 字符串,setContent 会替换掉整个编辑区内容,insertContent 则是在光标当前位置插入片段。
const editor = tinymce.get('editorArea'); // 取 HTML const html = editor.getContent({ format: 'html' }); // 取纯文本 const text = editor.getContent({ format: 'text' }); // 整体替换 editor.setContent('<p>新的内容</p>'); // 在光标处插入 editor.insertContent('<p>插入一段</p>'); // 监听内容变化 editor.on('change', () => { console.log('内容变化', editor.getContent()); });逻辑说明:getContent 的 format 参数很实用,做空值校验时用 text,做内容展示时用 html。setContent 默认不会触发 change 事件,如果你在组件里赋值后还要做联动,需要手动调用 editor.fire('change')。另外一个边界点:编辑器在 iframe 里,直接读原 textarea.value 拿到的永远是初始化前的旧值,这也是第 5 章要重点讲的转义问题来源之一。
3. 把一个可用的 TinyMCE 装进业务页面:步骤、代码与插件组合
3.1 最小化引入:从 HTML 结构到可编辑区域
先把资源包里的核心脚本放到页面里。TinyMCE 的核心文件是 tinymce.min.js,皮肤和插件目录要保持在同级目录结构下,否则初始化时控制台会报资源 404。最小化引入只需要一段 HTML 加一段 init。
<!DOCTYPE html> <html> <head> <script src="/libs/tinymce/tinymce.min.js"></script> </head> <body> <form id="articleForm"> <textarea id="articleContent" name="articleContent" placeholder="请输入正文"></textarea> <button type="submit">保存</button> </form> </body> </html>tinymce.init({ selector: '#articleContent', height: 500, menubar: false, plugins: 'lists link image', toolbar: 'undo redo | bold italic underline | bullist numlist | link image', promotion: false });逻辑说明:script 标签必须在 init 之前加载完成,这是最常见的翻车点。textarea 的 name 属性保留,是为了在没有初始化成功时表单还能兜底提交。init 里 menubar 设 false 去掉顶部菜单,promotion 设 false 去掉右下角官方标志,这两项都是后台管理系统里默认会关的。参数说明里,plugins 没有加载 code 插件,所以工具栏里也没写 code 按钮,保持功能与体积最小化。
3.2 表单场景:内容监听、提交校验与隐藏域同步
编辑器接管之后,表单直接提交拿不到最新内容。标准做法是把编辑器 HTML 同步到隐藏 input,再由隐藏 input 参与表单提交。同时监听 change 事件做实时预览。
const form = document.getElementById('articleForm'); const editor = tinymce.get('articleContent'); // 实时预览 editor.on('change', () => { document.getElementById('previewArea').innerHTML = editor.getContent(); }); // 提交前校验 form.addEventListener('submit', (e) => { const html = editor.getContent(); const pureText = editor.getContent({ format: 'text' }).trim(); if (!pureText) { e.preventDefault(); alert('正文内容不能为空'); return; } document.getElementById('hiddenHtml').value = html; });逻辑说明:校验时用 format: 'text' 而不是直接判断 html,因为<p><br></p>这种空内容在 HTML 里非空,但纯文本是空的。提交时把 html 写进隐藏域,后端拿到的就是完整带标签的富文本。注意这里不要再读 textarea.value,那个值在编辑器初始化后就是死数据了。参数说明里,change 事件覆盖用户输入、格式化和删除操作,但 setContent 不触发它,需要触发时手动调用 editor.fire('change')。
3.3 插件按需开:表格、图片、代码高亮怎么搭配
后台内容编辑最常用的组合是:列表增强、图片上传、表格、代码块、源码查看、自动增高。插件不是越多越好,每个插件都对应一段 JS 甚至 CSS,全部加载会让初始化变慢。下面是一组经过实际项目验证的配置。
tinymce.init({ selector: '#articleContent', plugins: 'lists advlist autolink link image table codesample code autoresize', toolbar: 'undo redo | blocks | bold italic underline | bullist numlist | table | codesample | image | code', height: 400, images_upload_url: '/api/upload/image', automatic_uploads: true, codesample_languages: [ { text: 'JavaScript', value: 'javascript' }, { text: 'HTML/XML', value: 'markup' }, { text: 'CSS', value: 'css' }, { text: 'SQL', value: 'sql' } ] });逻辑说明:advlist 扩展列表样式,autoresize 让编辑区随内容自动增高,codesample 负责代码块高亮,code 是查看/编辑 HTML 源码。automatic_uploads 加上 images_upload_url 后,用户粘贴或选择图片会自动走上传接口,不用自己写额外逻辑。codesample_languages 限定代码块可选语言,避免用户插入一堆编辑器不认识的语言。参数说明里,blocks 是 v6 工具栏里的块级标签选择器,替代旧版的 formatselect。
4. 框架集成:把 TinyMCE 封装成组件并打通上传链路
4.1 Vue 组件封装:受控组件思路与光标回填防抖
在 Vue 项目里直接写原生 init 最大的问题是生命周期:组件销毁时编辑器实例还在,路由切换后就会出现“找不到编辑器”的报错。封装成组件后,onMounted 里初始化、onBeforeUnmount 里销毁,数据流用 v-model 打通,这是最常见的做法。注意回填数据时要用防抖标志,否则编辑过程中外部数据一变,光标就被重置到开头。
<template> <textarea ref="editorRef"></textarea> </template> <script setup> import { ref, onMounted, onBeforeUnmount, watch } from 'vue'; import tinymce from 'tinymce'; const props = defineProps({ modelValue: { type: String, default: '' } }); const emit = defineEmits(['update:modelValue']); const editorRef = ref(null); let editor = null; let isTyping = false; onMounted(() => { tinymce.init({ selector: '#' + editorRef.value.id, plugins: 'lists link image table code', toolbar: 'undo redo | bold italic | bullist numlist | table', init_instance_callback: (ed) => { editor = ed; ed.setContent(props.modelValue || ''); ed.on('input change', () => { isTyping = true; emit('update:modelValue', ed.getContent()); isTyping = false; }); } }); }); watch(() => props.modelValue, (val) => { if (!isTyping && editor && editor.getContent() !== val) { editor.setContent(val || ''); } }); onBeforeUnmount(() => { if (editor) editor.remove(); }); </script>逻辑说明:init_instance_callback 里拿到实例引用,再用 editor.on 监听内容变化并 emit 出去,这样父组件的 v-model 能收到编辑器的每次输入。watch 里先判断 isTyping,再对比当前内容和 props 值是否一致,不一致才 setContent,能避免外部赋值打断用户输入。onBeforeUnmount 里 editor.remove() 是必须的,否则弹窗或路由跳转后编辑器残留,再次初始化时会出现重复绑定。参数说明里,selector 依赖 ref 的 id,保证每个组件实例初始化独立元素。
4.2 异步加载:减少首包体积的常见做法
TinyMCE 核心加常用插件打包后体积不小,放在首屏会影响加载速度。常见做法是路由进入编辑页再动态 import,配合 Vite 或 Webpack 自动分包,让编辑器独立成一个 chunk。
async function createEditor(selectorText, options) { const module = await import('tinymce'); const tm = module.default; await Promise.all([ import('tinymce/themes/silver'), import('tinymce/icons/default'), import('tinymce/plugins/lists'), import('tinymce/plugins/link'), import('tinymce/plugins/image') ]); tm.init({ selector: selectorText, ...options }); }逻辑说明:动态 import 会触发打包器把 tinymce 核心和插件拆成独立 chunk,等用户真正打开编辑器页面时才请求。Promise.all 里并行加载主题、图标和用到的插件,全部就绪后再 init。实际操作时,具体 import 路径要看项目打包器的解析结果,不同版本目录结构略有差异。参数说明里,如果某个插件没在 import 列表注册,即使 init 配置里写了 plugins,运行时也会报找不到插件。
4.3 图片上传对接:接口返回格式与错误处理
TinyMCE 的图片上传有两种方式:简单配置 images_upload_url,或者完全用 images_upload_handler 接管。后者更可控,前端可以加 token、处理错误、统一超时。后端接口返回格式必须严格是{ "location": "完整URL" },这是 TinyMCE 解析的固定字段。
tinymce.init({ selector: '#articleContent', plugins: 'image', toolbar: 'image', images_upload_handler: (blobInfo) => new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); fetch('/api/upload/image', { method: 'POST', body: formData, headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } }) .then((res) => { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); }) .then((json) => { if (json && json.location) { resolve(json.location); } else { reject(new Error('接口缺少 location 字段')); } }) .catch((err) => reject(err)); }) });逻辑说明:blobInfo.blob() 拿的是文件 Blob,filename() 是原始文件名,这两个都是编辑器内部自动生成的。FormData 的字段名要和后端接口约定一致,常见的就是 file。后端返回 location 字段后,resolve 给编辑器,编辑器把该 URL 插入 img 标签。如果接口返回的是 url 字段或者一个包含 url 的嵌套对象,前端要改成对应解析逻辑。参数说明里,headers 里加 token 是业务项目常态,后端也需要处理跨域和鉴权,否则接口 200 但图片加载不出来。
5. 避坑指南:TinyMCE 集成里反复出现的五个实际问题
5.1 初始化后编辑器空白或一直加载
现象:textarea 还在,但编辑器没有出现,或者出现一个空白 iframe,控制台报tinymce is not defined。
原因有两种:一是脚本加载顺序错了,init 执行时 tinymce 对象还不存在;二是 selector 没匹配到目标元素,编辑器静默失败。还有一种隐蔽情况是皮肤目录没上传到服务器,导致 iframe 里样式加载不出来,页面看起来像空白。
解决:script 标签放在 init 调用之前,或使用 defer 属性;init 之前用typeof tinymce === 'undefined'做一次判断;selector 写唯一 id,并确认元素在 DOM 里存在;打开浏览器 Network 面板,检查 skin 相关的 css 请求是否 404。
5.2 提交内容带着一堆转义符号
现象:后端存进数据库的内容是<p>段落</p>,前端展示时出现双重转义,页面把标签名显示出来了。
原因:表单提交时直接读了 textarea.value,而 textarea 在 HTML 里本来就有实体编码;或者是后端收到 HTML 后做了第二次转义。TinyMCE 接管后,textarea 不再同步内容,读它的 value 拿到的是初始化前的旧数据,容易误判。
解决:提交前强制走editor.getContent(),把结果写入隐藏 input,再让隐藏 input 参与提交;后端只做一次存储前的字符过滤,不要对富文本内容整体做实体转义。排查时在提交前后分别打印一次内容,对比哪一步开始出现<。
5.3 图片上传成功但编辑器里不显示
现象:上传接口返回 200,文件也在服务器上,但编辑器里图片区域是红色叉或空白块。
原因:最常见的是接口返回格式不是 TinyMCE 要求的{ location: "URL" };其次是返回的是相对路径,比如/uploads/1.jpg,页面在子路由下拼接出错;还有一种是编辑器默认用 blob 临时地址预览,页面刷新后 blob URL 失效,图片就裂了。
解决:用自定义images_upload_handler接管上传,拿到服务端真实地址后 resolve 给编辑器;后端返回绝对地址,或前端在 location 字段里拼接上域名;尽量不用 automatic_uploads 的默认行为,除非你已经确认后端返回格式完全匹配。
5.4 本地正常,部署到服务器后样式错乱
现象:本地编辑时字体显示正常,部署到测试环境后编辑区字体变小,工具栏按钮错位,代码块颜色缺失。
原因:本地开发服务器能解析相对路径,部署后如果 TinyMCE 的静态资源目录没有完整上传,或者 CDN 路径配错,skin 和插件样式就会失效。另一个常见原因是没配置 content_style,编辑区字体继承了 iframe 默认样式,与站点字体不一致。
解决:上传完整的 tinymce 目录,确认 skins、plugins 和 themes 三个子目录都在;引入 tinymce.min.js 建议用绝对路径或 CDN 完整地址,不要用相对路径;初始化时加上content_style: 'body { font-family: inherit; }',让编辑区字体与主页面保持一致。
5.5 多个编辑器实例互相覆盖或找不到实例
现象:页面里有两个编辑区,初始化第二个之后,第一个的配置变了;或者弹窗第二次打开时控制台报编辑器 id 重复。
原因:init 时 selector 用了相同的 id;或者弹窗关闭后没有销毁编辑器实例,再次打开时旧实例还在占用同一个 id。TinyMCE 内部通过 id 管理实例,重复注册会互相覆盖。
解决:每个编辑器使用唯一的 id,不建议用 class 初始化多个;弹窗关闭时调用tinymce.remove('#editorId');初始化前先判断tinymce.get('editorId')是否存在,存在就先 remove 再重新 init。组件化封装时,这个清理动作要放在 onBeforeUnmount 里,形成固定习惯。
6. 上线前再调一遍:内容净化、自定义按钮与验证检查
6.1 内容净化:对拿回来的 HTML 做白名单过滤
TinyMCE 默认会过滤部分危险标签,但它面向的是编辑器内部操作,挡不住通过源码视图直接粘贴的恶意内容。我一般会在初始化配置里直接限定合法标签,形成一个内容白名单,而不是靠后端单独处理。
tinymce.init({ selector: '#editorArea', valid_elements: 'p,br,strong,em,u,h2,h3,ul,ol,li,a[href|target],img[src|alt|title]', invalid_elements: 'script,iframe,style,form', setup: (ed) => { ed.on('form submit', () => { const html = ed.getContent(); // 这里可以再加一层 DOM 白名单过滤 console.log('提交前净化', html); }); } });逻辑说明:valid_elements 是白名单,只有列出的标签会保留,a 标签允许 href 和 target 属性,img 允许 src、alt、title;invalid_elements 是黑名单,两者同时配置时以白名单为准。这个方案能挡掉大部分 script 注入,但服务端仍然要做同样的过滤,前端只是减少脏数据进入数据库的概率。参数说明里,不要只依赖 invalid_elements,攻击者换个标签写法就能绕过黑名单。
6.2 添加一个自己的工具栏按钮
编辑器集成到业务里,迟早会遇到“加一个自定义按钮”的需求。最常见的是插入当前日期、插入特殊占位符、调用业务弹窗。v6 的注册方式是通过ui.registry.addButton,注册后再在 toolbar 里引用按钮名。
tinymce.init({ selector: '#editorArea', plugins: 'lists link', toolbar: 'undo redo | myInsertDate', setup: (ed) => { ed.ui.registry.addButton('myInsertDate', { text: '插入日期', onAction: () => { const date = new Date().toLocaleDateString('zh-CN'); ed.insertContent(`<p>${date}</p>`); } }); } });逻辑说明:setup 回调里注册按钮,是 v6 推荐的自定义扩展方式。addButton 第一个参数是按钮名,对应 toolbar 里的标识;text 是按钮显示文字;onAction 是点击回调。ed.insertContent 会把内容插到当前光标位置,比直接拼接 HTML 更安全。参数说明里,如果想要图标按钮,可以改用 icon 属性并传入注册过的图标名,否则就用 text 最省事。
6.3 上线前的验证检查清单
一次编辑器集成是否可靠,上线前按下面这张表过一遍,能省掉大部分线上翻车。
| 检查项 | 验证方式 | 常见故障 |
|---|---|---|
| 核心脚本与语言包 | Network 面板确认 200 | 404 导致初始化失败 |
| skin 与插件资源 | 检查 skins/plugins 目录 | 样式错乱、按钮缺失 |
| 图片上传接口 | 选图上传后看返回 | location 字段缺失 |
| XSS 过滤 | 源码模式粘一段 script | 内容被存储执行 |
| 多浏览器 | Chrome/Firefox/Edge 各过一遍 | 工具栏布局差异 |
| 表单提交取值 | 提交后打印最终 HTML | 双转义、旧值覆盖 |
| 组件销毁 | 路由切换后再进编辑页 | 实例残留报错 |
最后一件事是我自己的血泪教训:早前做某内容发布平台,上线前只测了正常输入,漏了“从源码视图粘贴外部 HTML”这条路,结果有人把带事件属性的代码存进了数据库。从那以后我每次集成编辑器,都会强制走一遍“粘贴脏数据—提交—展示”三条路径,再检查一次白名单配置。编辑器的坑多数不是它本身复杂,而是你默认它替你做了过滤和资源管理,实际上这两件事都得自己兜底。希望帮到你。
本文还有配套的精品资源,点击获取