简介:TinyMCE v6.2.0 是一套完整开源富文本编辑器源码资源,专为需要在 Web 项目中实现所见即所得编辑功能的开发者准备,也可作为前端源码分析、毕业设计或计算机课程案例的实用素材。资源共 64 个文件,压缩包约 724KB,以 37 个 js 核心模块与 22 个 css 样式表为主,辅以 ts 类型声明、htm 说明页及 md 文档,既可直接集成到站点,也便于阅读内部实现。此版本针对性能和交互体验进行了迭代优化,适合作为内容管理系统、论坛或博客平台的底层编辑组件。已有 187 人浏览学习,适合不同层次的读者取用。通过解压后的说明文件可明确 v6.2.0 相比旧版的更新点,借助源码和插件机制可扩展自定义按钮、格式控制与媒体嵌入;学习 API、事件和跨浏览器兼容处理,也能帮助理解现代前端编辑器架构,为论文或实际项目提供可靠参考。 我电脑里到现在还存着这个TinyMCE编辑器 v6.2.0.zip,不是没删,而是里面那个解压后的tinymce目录几乎每次做后台管理项目都会原封不动拷过去。起因是去年接了一个老系统的运维需求,内外网隔离,npm registry 完全不可用,所有前端依赖都要手工拷进去,我就在官网下了这个 zip 包。一开始只是应急,结果这套目录结构反而成了我最顺手的富文本方案,官网的 zip 包自带所有插件和皮肤,比 npm 里拆得零零碎碎的包要好理解得多。
这篇文章就把我从下载TinyMCE v6.2.0到在真实项目里落地、踩坑、最终稳定运行的全过程写出来,包括目录结构怎么部署、初始化参数怎么调、图片上传为什么经常"不显示"、Vue2 项目里怎么封装最稳妥。适合刚接触 TinyMCE 的前端,也适合那些要在内网环境、离线环境里做内容管理的同学。
1. 为什么是 TinyMCE 6.2,而不是 Quill 和 Markdown 编辑器
先说选型。那阵子我手头同时评估过几个富文本方案,网上随便一搜就是一大片对比帖:Quill 轻量、Markdown 编辑器足够简单、TinyMCE 功能全面。但落到我这个老系统场景里,结论其实非常明确。
| 方案 | 打开即用的完整度 | 图片上传/文件管理 | 适用人群 | 离线集成难度 |
|---|---|---|---|---|
| 原生 contenteditable | 最低,全部要自己写 | 自己造轮子 | 没有参考价值 | 低 |
| 轻量 Markdown 编辑器 | 中,语法体验好 | 需要额外扩展 | 技术向用户 | 中 |
| Quill | 中,核心干净、扩展靠 module | 官方模块不太够用 | 前端技术背景 | 中 |
| TinyMCE 6 | 高,插件和 UI 开箱即用 | 自带 image/file 插件流程 | 业务系统后台、非技术用户 | 低 |
Quill 的问题在于它太"骨架"了,我要给运营团队用的编辑器,图文混排、表格、插入代码、全屏、粘贴 Word 内容这些能力都得具备,Quill 默认工具栏离这个期望还很远,得花不少时间去挑 module、测试兼容性。Markdown 编辑器更直接,虽然它很适合写技术文档,但让运营同学记这种语法,第二天就会被骂回来。TinyMCE 最接近"打开就是 Word 里那段工具栏"的体验,对非技术用户几乎没有学习成本。
再说版本。TinyMCE 6 相比 v5 最大的变化是把皮肤、主题、图标资源做了更彻底的拆分,插件也一口气合并掉了几个老插件,整体体积反而比 v5 小。6.2.0 是走完了 v6 早期迭代的一个相对稳定版本,我实际用下来没有遇到显著的 bug。更关键的是,官网提供了打包好的 zip,解压之后tinymce.min.js、plugins/、themes/、skins/、icons/全部就位,不需要依赖 npm 安装过程,这在离线内网环境里是决定性的优势。
你如果只是想在普通项目里用个富文本,npm 装tinymce肯定更快;但如果你的部署环境是严格的内网,或者你希望完全掌握源代码和静态资源,zip 包这条路几乎是唯一能走通的方式。这篇文章的场景假定,就是你已经拿到了TinyMCE编辑器 v6.2.0.zip,正准备把它部署到自己的项目里。
2. 解压后部署:目录结构和最容易踩的三个坑
zip 解压出来第一眼会觉得东西很多,但其实真正要复制进你项目的只是js目录下面那一层。标准的 TinyMCE 6 zip 包解压后路径大概是:
tinymce_6.2.0/ ├── icon.png ├── LICENSE.txt ├── readme.md └── js/ └── tinymce/ ├── tinymce.min.js ├── tinymce.js ├── license.txt ├── plugins/ # 插件目录,按插件名分文件夹 ├── themes/ # 主题目录,一般是 silver ├── skins/ # 皮肤目录,oxide / oxide-dark ├── icons/ # 图标目录 └── models/ # 部分新版本才有,不影响使用我见过有人图省事把解压出来的所有文件直接扔到静态资源根目录,结果页面里script标签写到js/tinymce/tinymce.min.js才发现层级不对。我的习惯是把这个tinymce父目录整个拷到项目public/tinymce下,线上访问路径就是/tinymce/tinymce.min.js,插件路径则自动解析为/tinymce/plugins/,逻辑清晰。
2.1 坑一:皮肤目录被误删,编辑器直接初始化失败
TinyMCE 6 初始化时会通过skin配置项加载皮肤,默认是oxide。如果你的部署目录里没有skins/ui/oxide/下的skin.min.css和content.css,浏览器控制台会报类似Failed to load skin: oxide的错误,编辑器区域直接白板。我从朋友的工位上看到过他把"用不到的多余文件"删掉,删完编辑器立刻白屏,他一脸懵。这个目录不是冗余,是核心资源。
2.2 坑二:直接用 file:// 协议打开页面,插件和图标跨域加载失败
解压完成后如果只是双击index.html想预览效果,大概率会看到编辑器容器里什么都没有,控制台飘红一片。原因是file://协议下浏览器对本地文件发起的 XHR 和模块加载有严格的跨域限制,TinyMCE 去拉取插件、皮肤、语言包时会被拦截。这个坑对刚上手的人太常见了,解法也简单:起一个本地静态服务,比如npx serve或python -m http.server 8080,然后通过http://localhost:8080访问页面。
2.3 坑三:默认只有英文,忘了放语言包
TinyMCE 6 的 zip 包里默认不带中文语言包,官方语言包需要单独下载,解压后得到一个zh_CN.js文件。很多人部署完发现工具栏是英文,以为要下载一个"汉化版",其实不用,把zh_CN.js放进一个langs目录,初始化时指定language: 'zh_CN'和language_url指向这个文件就完事了。注意如果你用的是内网环境,language_url不能写外链 CDN,否则还是要手动把zh_CN.js拷到自己的静态目录里。
另外提一句,zip 包里readme.md写得已经够清楚了,但大多数人都不会先看它。如果你拿到的是_dev后缀的开发包,源码未压缩,部署到生产环境时建议换成正式包,能少几百 KB 的体积。
3. 初始化参数:按需加载,把编辑器调成顺手的样子
TinyMCE 的核心用法就一个全局方法:tinymce.init。它接收一个配置对象,所有功能开关、外观参数都在这个对象里。项目里我最常用的初始化配置大概是这样的:
tinymce.init({ selector: '#editor', language: 'zh_CN', language_url: '/tinymce/langs/zh_CN.js', height: 500, menubar: 'file edit view insert format tools table help', plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount', toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | removeformat | image media table | fullscreen help', convert_urls: false, branding: false });3.1 plugins 和 toolbar 是两套体系,很多人搞混
第一次用 TinyMCE 的人最常犯的错是把toolbar里写了一大堆按钮名,但没在plugins里声明对应插件,结果按钮显示不出来。TinyMCE 的机制是plugins决定"加载了哪些功能模块",toolbar决定"把哪些按钮画在工具栏上"。比如你要用image按钮,就必须先在plugins里带上image,否则工具栏上那个位置会是一个缺图标的小方块。反过来也是,插件加载了但不往工具栏放,功能菜单里可能还会出现,由menubar控制。
v6 的 toolbar 写法可以按分组配置,每个组之间用|分隔。注意blocks这个按钮是 v6 里用来快速切换标题、段落格式的入口,替代了 v5 时代常用的formatselect,如果你看到一个示例代码里用formatselect,它在 v6 里其实继续兼容,但我建议新代码直接写blocks,UI 更现代,下拉选项也更合理。
3.2 内容样式跟编辑器 UI 样式是两回事
你可能会发现编辑器外观皮肤是好了,但编辑区里正文的字号、行距、链接颜色总是差强人意,这是因为内容区样式走的是content_css,不是skin。默认情况下 TinyMCE 会加载skins/content/default/content.css,如果你希望正文区域内文字风格和你的网站整体风格一致,可以自己写一份内容样式文件,然后在content_css里指定:
tinymce.init({ selector: '#editor', content_css: '/css/editor-content.css' });这里要留意一个细节:如果content_css指定了多个文件,TinyMCE 会用逗号分隔的字符串,而不是数组。官方文档里写的是空格分隔,实际我用的稳定写法是用逗号分隔,比如'/css/base.css,/css/editor-content.css'。
3.3 按需加载插件,不要一把梭
zip 包的plugins目录里大概有几十个插件,但项目里不需要全加载。插件越多,初始化时加载的 JS 文件越多,首屏时间就越长。我总结过一套适合内容管理后台的"菜单":autolink lists link image media table这些是必备的,code和fullscreen是后台用户的隐藏需求,wordcount用来给运营同学展示字数统计,用了都说好。其余什么charmap、emoticons、anchor看业务需要再加。
另外,如果你用的是tinymce.min.js这个全量入口,插件其实不像 npm 模块化那样按需打包,它是在运行时按需从plugins/目录加载对应 JS 文件。所以"按需"主要体现在你配置了哪些插件,TinyMCE 只去请求那些插件的脚本,放在plugins目录里但不声明的插件不会被加载,这一点在内网部署时也友好,没有那个插件目录的机器只要不声明也不会报错。
4. 图片上传"不显示"的全链路排查
如果你在网上搜过编辑器相关的问题,大概率看到过"js+html+编辑器添加图片不显示"这种搜索词,我自己也被这个问题折磨过,而且它分好几种场景,原因完全不同。
4.1 场景一:图片是插进去了,但刷新页面就没了
这是最常见的现象:用户在编辑器里粘贴或拖入一张本地图片,当时看是有的,点了保存、刷新页面,图片裂了,或者编辑器初始化后内容里只剩一个空的img标签。原因很简单,TinyMCE 默认会把本地图片转成blob:URL 存到一个临时的 BlobCache 里,这个 URL 只在当前页面会话内有效,刷新后自然就找不到了。
所以正确做法是配置图片上传,让图片落库到你的服务器或 OSS,而不是留在浏览器内存里。两种配置方式:
第一种,指定后端接收地址:
tinymce.init({ selector: '#editor', images_upload_url: '/api/images/upload' });后端接口返回的 JSON 必须长这样,否则 TinyMCE 识别不了:
{"location": "https://cdn.example.com/uploads/2025/01/abc.png"}第二种,更灵活的方式,用images_upload_handler自己控制上传过程,适合要带 token、自定义 headers、或者要走 COS/OSS 直传的场景:
tinymce.init({ selector: '#editor', images_upload_handler: (blobInfo, progress) => { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('file', blobInfo.blob(), blobInfo.filename()); fetch('/api/images/upload', { method: 'POST', body: formData, headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') } }) .then(response => response.json()) .then(data => { if (data && data.location) { resolve(data.location); } else { reject('上传接口返回格式不正确,需要包含 location 字段'); } }) .catch(error => reject(error)); }); } });细心的同学会发现,resolve里的字符串会被 TinyMCE 直接当作图片的src,所以后端返回的location必须是完整可访问的 URL,或者相对路径也请确保在项目里能正确解析。
4.2 场景二:图片上传接口成功,但页面里还是不显示
这种情况就更有意思了。接口明明返回了正确的location,编辑器里却是一张破图。我排查下来原因有很多,最常见的是没看后台返回的数据到底长什么样。比如后端返回了{url: "..."}而不是{location: "..."},TinyMCE 拿不到location,只好留一个空src,图片自然不显示。
还有一种是后端的location返回了相对路径,比如/uploads/xxx.png,但当前页面部署在一个二级目录下,比如https://example.com/admin/editor,这个相对路径解析后就访问到不存在的地址。这时候检查一下 Network 面板,看图片请求的实际 URL 跟预期是否一致。
4.3 场景三:图片源跨域或者被 CSP 拦了
还有一种隐蔽的情况,图片服务在另一个域名,比如https://cdn.internal.example.com,而你的网站Content-Security-Policy配置里没有放开img-src,浏览器会把图片请求拦下来。这时候编辑器里显示空白,但如果你右键检查元素,图片src看起来是完好的。我的排查顺序是:先在浏览器里直接打开图片 URL,确认能访问;再看 Network 面板里图片请求是否被csp拦截;最后看返回的图片是不是 403 或 302 跳转到了登录页。
TinyMCE 还贴心地提供了一个配置images_upload_base_path,可以把上传后的相对路径统一加上前缀,适合图片接口返回相对路径的场景。但这属于补救措施,最好还是让后端直接返回完整的可访问地址。
4.4 还有一个最容易忽略的:编辑器容器隐藏时初始化
有些页面里的 Tab 是默认隐藏的,切到 Tab 时才初始化编辑器。如果编辑器在一个隐藏容器里初始化,TinyMCE 计算高度和位置时会拿到 0,图片加载后也可能出现布局错乱甚至"不显示"的错觉。这个场景不是图片上传逻辑的问题,而是编辑器初始化时机问题。解法是等容器可见后再tinymce.init,或者在切换 Tab 后调用一次editor.fire('ResizeEditor')让编辑器重新计算尺寸。
5. Vue2 里接 TinyMCE 的实测记录
那段时间我正好要在 Vue2 项目里落地这个编辑器。网上有两个比较常见的方案:vue-tinymce和@tinymce/tinymce-vue。但前者版本老,适配 v6 不理想;后者官方包装的是 Vue3 和 Vue2.7+,对老项目里的 Vue2.6 兼容性要看运气。我自己最后选了手动封装一个组件,反而最稳。
5.1 用 zip 包而不是 npm 包,怎么在 Vue2 里引入
既然场景是 zip 包部署,那么最直接的方法是把tinymce静态目录放到public下,然后在index.html里用普通script标签加载:
<script src="/tinymce/tinymce.min.js"></script>这样全局挂载window.tinymce,组件内直接使用即可。好处是不用管 npm 包依赖关系,也不会发生tinymce requires jQuery这类老掉牙的问题。然后封装一个通用组件:
<template> <textarea :id="editorId" v-model="innerValue"></textarea> </template> <script> export default { name: 'TinyEditor', props: { value: { type: String, default: '' }, plugins: { type: Array, default: () => ['lists', 'link', 'image', 'table', 'code', 'fullscreen'] } }, data() { return { editor: null, editorId: 'tmce-' + Math.random().toString(36).slice(2, 9) } }, computed: { innerValue: { get() { return this.value }, set() {} } }, mounted() { this.editor = window.tinymce.init({ selector: `#${this.editorId}`, height: 400, language: 'zh_CN', language_url: '/tinymce/langs/zh_CN.js', plugins: this.plugins, toolbar: 'undo redo | blocks | bold italic forecolor | bullist numlist | image table | fullscreen', setup: (editor) => { this.editor = editor editor.on('input change undo redo', () => { this.$emit('input', editor.getContent()) }) } }) }, beforeDestroy() { if (this.editor && this.editor.initialized) { this.editor.destroy() } } } </script>注意这里computed里的innerValue其实只是让textarea的初始内容能渲染出来,真正的内容变化不靠v-model双向绑定,而是通过editor.on监听各种内容变更事件再把内容$emit出去。这样父组件拿到的是编辑器里干净的 HTML 字符串,不会出现"光标一动 Vue 就重新渲染把内容覆盖掉"的经典冲突。
5.2 内容回显与双向同步的细节
父组件里这样用:
<template> <TinyEditor v-model="content" /> </template>如果要回显编辑内容,比如做文章编辑功能,只需要给组件传:value="article.content"。但有一个问题:如果父组件异步加载了文章内容,value从空字符串变成有值,Vue 的 prop 更新了,编辑器内部的textarea却不会自动跟着变。这时候需要在组件里watch这个value:
watch: { value(newVal) { const editor = this.editor if (editor && editor.initialized) { const current = editor.getContent() if (newVal !== current) { editor.setContent(newVal) } } } }加一个newVal !== current的判断是为了避免循环:编辑器内容变化会 emit 给父组件,父组件再传回来,如果 setContent 刷一次内容,触发 change 事件,又 emit 一次,就可能死循环。踩过一次坑之后我特别强调这个判断。
5.3 性能实测:本地静态包比走 CDN 稳
有人喜欢直接引官方 CDN 的 TinyMCE,这样少维护一份文件。但我在实际项目里发现,国内网络访问官方 CDN 并不稳定,有些用户集中在偏远地区或者公司内网,CDN 资源超时的情况时有发生。而且官方云服务的域名在生产环境里可能被代理规则拦掉。所以哪怕是非内网项目,我也更推荐把 zip 包里的js/tinymce目录拷到自己的静态资源服务里,自己控制缓存,加载速度反而更可控。
另外有一个提进度的小技巧:编辑器其实可以按需延迟初始化,不需要在页面加载时就初始化。比如弹窗里才打开的编辑器,等弹窗打开时再调用init,这样能省掉首屏一部分网络请求和解析时间。如果你用@tinymce/tinymce-vue这类封装库,官方文档里也有 lazy-loading 的示例,原理就是动态加载tinymce.min.js,加载完再去 init。
5.4 组件销毁时一定记得 destroy
Vue2 里最容易漏的是组件销毁后,编辑器实例还留在内存里。我的做法是beforeDestroy里找到编辑器实例并destroy。如果你在页面上同时放了好几个编辑器,不销毁的后果是切路由后 DOM 已经没了,但编辑器实例还在,再来一次访问时可能出现重复初始化或者事件监听堆积,时间长了页面会越用越卡。
如果是弹窗里的编辑器,有一种更简单的做法:弹窗每次打开都重新init,关闭时destroy,完全不留隐患。一开始多写两行代码,后面维护的时候能省很多事。
最后再分享一个小技巧
我经常看到有人把 TinyMCE 初始化的selector写成一个 id,其实也支持 class。如果你在一个列表页里循环渲染多个编辑区域,可以用同一个 class 做 selector,一次init就能批量初始化。缺点是这样不好分别监听每一个编辑器的输入事件,所以批量场景我更建议每个编辑器实例单独初始化、单独监听,维护起来不至于一锅粥。
TinyMCE 这个 zip 包我用了很久,从 v5 一路到 v6,体验最明显的是 v6 的 UI 干净了很多,而且插件拆分之后加载逻辑更清晰。如果你正在后台管理项目里为富文本编辑器发愁,拿这个 v6.2.0 的包,按上面这套流程部署初始化,基本能少走一半弯路。
本文还有配套的精品资源,点击获取