公司动态

TinyMCE 6.2离线部署实践:从zip包到Vue2封装全记录

📅 2026/9/1 20:46:14
TinyMCE 6.2离线部署实践:从zip包到Vue2封装全记录
简介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 编辑器更直接虽然它很适合写技术文档但让运营同学记![alt](url)这种语法第二天就会被骂回来。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 里其实继续兼容但我建议新代码直接写blocksUI 更现代下拉选项也更合理。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. 图片上传不显示的全链路排查如果你在网上搜过编辑器相关的问题大概率看到过jshtml编辑器添加图片不显示这种搜索词我自己也被这个问题折磨过而且它分好几种场景原因完全不同。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 :ideditorId v-modelinnerValue/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-modelcontent / /template如果要回显编辑内容比如做文章编辑功能只需要给组件传:valuearticle.content。但有一个问题如果父组件异步加载了文章内容value从空字符串变成有值Vue 的 prop 更新了编辑器内部的textarea却不会自动跟着变。这时候需要在组件里watch这个valuewatch: { 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 组件销毁时一定记得 destroyVue2 里最容易漏的是组件销毁后编辑器实例还留在内存里。我的做法是beforeDestroy里找到编辑器实例并destroy。如果你在页面上同时放了好几个编辑器不销毁的后果是切路由后 DOM 已经没了但编辑器实例还在再来一次访问时可能出现重复初始化或者事件监听堆积时间长了页面会越用越卡。如果是弹窗里的编辑器有一种更简单的做法弹窗每次打开都重新init关闭时destroy完全不留隐患。一开始多写两行代码后面维护的时候能省很多事。最后再分享一个小技巧我经常看到有人把 TinyMCE 初始化的selector写成一个 id其实也支持 class。如果你在一个列表页里循环渲染多个编辑区域可以用同一个 class 做 selector一次init就能批量初始化。缺点是这样不好分别监听每一个编辑器的输入事件所以批量场景我更建议每个编辑器实例单独初始化、单独监听维护起来不至于一锅粥。TinyMCE 这个 zip 包我用了很久从 v5 一路到 v6体验最明显的是 v6 的 UI 干净了很多而且插件拆分之后加载逻辑更清晰。如果你正在后台管理项目里为富文本编辑器发愁拿这个 v6.2.0 的包按上面这套流程部署初始化基本能少走一半弯路。本文还有配套的精品资源点击获取