Vue项目中Tinymce自定义本地视频上传完整指南
说到富文本编辑器Vue项目里在地选择Tinymce的人真不少我也是被它的扩展性吸引的。但前阵子做某知识库后台客户要求用户能直接从本地选择视频传到编辑器里并且要在文章里正常播放。原以为Tinymce自带的媒体插件能搞定结果默认弹窗只支持填视频网址本地文件的按钮根本不存在。最后花了不少时间走了一遍“选文件、传服务器、插入编辑器”的完整链路中间踩了大大小小一堆坑这里完整整理出来。这套方案不依赖某个特定后端核心是Tinymce的file_picker_callback入口加上自己实现的上传逻辑适配Vue 2和Vue 3。文章适合正在用Tinymce做内容系统、需要支持本地视频上传的前端同学也可以作为图片上传的参考只是文件类型过滤和回插参数不同。1. 先搞清楚Tinymce的视频上传逻辑1.1 默认媒体弹窗到底做了什么Tinymce的media插件负责“插入/编辑视频”功能。点工具栏的视频按钮弹出的对话框里只有一个URL输入框让用户填视频链接或者在线视频嵌入代码。它本身不提供“浏览本地文件”的入口这是很多刚接触的人第一个懵的地方。这个设计其实合理编辑器只是一个内容编辑壳子不负责文件存储。所有通过URL引用的资源都假定已经存在于某个可访问的地址上。图片上传之所以“开箱即感受不错”是因为不少项目额外配置了image_list、images_upload_handler之类的钩子文件被传到了自己的对象存储或服务器。而视频资源体积大、接口不同、鉴权复杂Tinymce官方刻意不越权帮你做。所以结论很明确想要在Vue项目里使用Tinymce编辑器自定义本地上传视频必须自己接管文件选择、上传接口调用、URL回插这三个环节。media插件保留但它只负责最终视频标签的渲染和编辑。1.2 file_picker_callback是绕不过去的入口Tinymce在打开媒体对话框时会检查初始化配置里的file_picker_callback。如果存在用户点“插入/编辑媒体”时原本只有URL输入框的界面之外会额外出现一个“从本地选择”类型的能力入口。这个回调接收三个参数callback、value、meta。meta里最关键的是filetype常见取值有image、media、file。当用户是在媒体弹窗里点文件选择meta.filetype就是media。你在这个回调里创建一个隐藏的input[typefile]并触发选择拿到本地文件后执行上传最后把得到的URL通过callback回传给编辑器。需要注意的是这个回调同样会响应图片选择。所以判断meta.filetype时不能写死不能不论什么类型都走视频上传逻辑否则点图片也会弹视频选择框。2. Vue项目里的接入思路与基础配置2.1 为什么推荐封装成独立上传模块我在实际项目里不建议把上传逻辑直接写死在编辑器初始化配置里。一个后台系统的编辑器可能出现在文章发布、活动配置、个人简介等多个页面每个页面都要用视频上传如果逻辑都塞在组件里后期改接口、加校验会非常痛苦。我习惯的拆分方式是把“上传视频”抽成一个单独的JS模块专门负责文件格式校验、大小校验、FormData组装、调用接口、返回URL。编辑器组件里只负责拿到file对象调这个模块再处理回调结果。这样Vue页面代码干净遇到视频上传出问题也能单独测试。这里也涉及一点工程化考虑Tinymce初始化配置是一个普通对象但它内部很多回调函数不是在Vue响应式系统里运行的。如果你在回调里直接访问组件data可能会出现闭包捕获旧值、this指向丢失等问题。把上传逻辑放到外部工具函数里可以避免这些响应式副作用。2.2 在初始化配置里挂上自定义媒体选择器使用tinymce-vue封装组件时核心是init配置。先安装依赖npm install tinymce tinymce/tinymce-vue然后引入需要的插件和主题把file_picker_callback配进去。下面是一个基础骨架后面会逐步补全。import tinymce from tinymce/tinymce import tinymce/themes/silver/theme import tinymce/icons/default import tinymce/plugins/media import tinymce/plugins/link import tinymce/plugins/image import tinymce/plugins/table import tinymce/plugins/lists const initOptions { plugins: media link image table lists, toolbar: media image ..., media_live_embeds: true, file_picker_callback: (callback, value, meta) { // 在这里处理本地视频选择 } }只配置到这里还不行file_picker_callback要做的第一件事就是判断meta.filetype。判断通过后生成文件输入框并模拟点击这是所有自定义上传的基础套路。3. 本地视频上传三步走选文件、传接口、回插编辑器3.1 第一步拿到文件对象并做基础校验无论UI交互是点击按钮还是拖拽上传核心都是拿到一个File对象。我在file_picker_callback里通常直接创建一个input元素设置accept为video/*然后触发click。file_picker_callback: (callback, value, meta) { if (meta.filetype ! media) return const input document.createElement(input) input.type file input.accept video/* input.style.display none document.body.appendChild(input) input.click() input.onchange async () { const file input.files input.files[0] document.body.removeChild(input) if (!file) return // 校验 } }文件校验不要只信扩展名。有些用户会把MP4改名为MP3后台系统容易被传一堆垃圾。校验类型我通常会限定video下的常见MIMEconst allowTypes [ video/mp4, video/webm, video/ogg, video/x-m4v, video/quicktime ] if (!allowTypes.includes(file.type)) { console.warn(不支持该视频格式) return } if (file.size 200 * 1024 * 1024) { console.warn(视频超过200MB请先压缩或改用分片上传) return }大小限制要根据自己后端和服务器带宽来定。如果项目没有对象存储别为了通吃就放开到1GB后端接口超时、服务器磁盘被写满是早晚的事。3.2 第二步把视频通过FormData上传到服务端拿到合法文件后上传方式很多最通用的是FormData加axios。视频文件大要调整超时时间默认30秒肯定不行。import request from /utils/request async function uploadVideo(file, onProgress) { const formData new FormData() formData.append(file, file) formData.append(bizType, article_video) const res await request.post(/upload/video, formData, { headers: { Content-Type: multipart/form-data }, timeout: 600000, onUploadProgress: (e) { if (onProgress) onProgress(e) } }) return res.data.data.url }这里有一个容易踩的细节后端返回的URL可能是相对路径也可能是全路径。全路径通常包含文件域名但如果是内网环境或前端页面部署在CDN之后全路径反而会跨域。我这边一般让后端返回可在前端直接拼接的相对路径或者返回文件唯一ID由前端拼出完整地址。如果项目视频量大建议后端在上传接口里返回视频尺寸、时长、封面截图地址前端一次性存进编辑器数据结构。虽然这一步不是必须但文章列表页需要视频封面时你会感谢当时多存了这些字段。3.3 第三步把上传得到的URL回插给Tinymce上传成功后调用file_picker_callback的第一个参数callback把URL传给编辑器。关键点来了callback不只是一个字符串很多情况下需要同时传视频源信息和展示属性。callback(url, { source: url, poster: coverPoster, width: 100%, height: 400, controls: true })第二个参数里的source表示真实视频地址。如果你只传一个字符串URLTinymce也能插入视频但有些版本会丢失controls、poster等属性。传一个对象更保险。另外要注意callback调用完成后用户可能还会继续编辑这个视频。比如改尺寸Tinymce会重新走file_picker_callback去获取新的视频地址。如果此时回调里没有保留旧值用户会感觉“明明选了视频一改属性怎么丢了”。因此file_picker_callback里的value参数要用起来它是当前已有的视频URL可以在callback里作为默认值回显。4. 我踩过的坑和解决记录4.1 视频文件太大导致接口超时与预览卡顿第一次测试时我选了一个300多MB的MP4前端axios设置的120秒超时结果后端从对接存储到自己服务器花了快两分钟直接超时失败。更麻烦的是即使上传成功用户在编辑器里预览也要从服务器拉整个文件体验非常差。后来我做的调整是三个方向并行。第一前端限制文件大小超过200MB就提示用户先压缩或者走单独的大文件上传页面。第二后端做转码和切片前端拿到的是转码后的标准MP4地址。第三上传接口增加进度条避免用户以为页面卡死。如果你的项目没有转码条件至少要让用户选完文件后有个明确的进度提示。FormData上传要知道进度很容易axios的onUploadProgress就能拿到loaded和total可以画一个简单的进度条或者显示百分比。4.2 二次打开编辑时视频源丢失这个问题最坑现象是第一次插入视频完全正常但保存后再次打开编辑文章视频区域要么是空白要么只有一个链接。排查了后端数据发现HTML里明明有video标签src也在但Tinymce初始化后媒体弹窗里显示不了。原因是Tinymce的media插件对视频的解析依赖data-mce-src、data-mce-poster等自定义属性。如果后端富文本过滤或数据库存储时把这些属性丢了或者前端在服务端渲染时给清洗掉了编辑器就识别不出这个视频。解决方法是保存内容时不要对内容做过于激进的自定义HTML清洗尤其是保留data-*属性。同时初始化编辑器时可以把内容字符串调试打印出来确认video标签是否完整。这个坑通常不是Tinymce的问题而是项目里现成的HTML过滤器太“勤劳”。4.3 视频自动播放和尺寸控制需要额外处理客户希望文章里的视频自动播放结果简单在init配置里设置autoplay属性没用。因为Tinymce的media插件默认不允许所有媒体属性autoplay、playsinline这类属性会被过滤或者只在某个条件下生效。我最后的处理是在保存内容时用正则给video标签补偿属性或者更稳妥一点在初始化时给媒体弹窗的模板补充自定义字段。如果只追求最终显示效果可以在渲染文章详情页时不直接渲染富文本内容而是先处理video标签给每个视频包裹一层容器再设置autoplay。实际项目中我倾向于后者因为编辑器里是否自动播放不重要重要的是文章正式发布后播放器行为可控。编辑器里保留了原始资源和宽高属性阅读页统一处理反而少踩坑。4.4 后端返回视频地址含有特殊字符导致插入错误视频文件名称如果带着中文、空格、#号后端拼接URL时又没有做URL编码src会变成不可用的地址。头一次测的时候文件名是“企业培训 第1讲.mp4”上传成功后端返回的地址里空格直接就成了非法字符视频加载不出来。这个问题解决不复杂但容易被忽略。前端在传给后端时可以强制重命名文件用时间戳加随机串file.name太小众的字符多干脆弃用原名。后端返回时也要确保地址是编码后的至少空格要转成%20中文要做encodeURIComponent。检查时可以手动在浏览器打开后端返回的地址如果404多半是编码问题如果能打开就是前端回插参数的问题。5. 常见问题速查与排查清单5.1 高频问题对照表这里整理了一份我实际遇到过、或者被问过的短视频上传高频问题排查思路基本都在表格里。问题现象可能原因处理办法弹窗里没有文件选择入口file_picker_callback未配置或meta.filetype判断错误检查init配置确认是media类型accept设置成video/*还是能选图片部分浏览器input accept过滤不严格选中后增加file.type白名单校验上传成功但视频黑屏转码后地址失效或视频编码不符合浏览器标准检查播放地址确认后端输出MP4 H.264编辑保存后视频丢失>// utils/videoUpload.js import request from /utils/request const MAX_SIZE 200 * 1024 * 1024 const ACCEPT_TYPES [video/mp4, video/webm, video/ogg, video/x-m4v, video/quicktime] export async function uploadVideo(file, onProgress) { if (!file) throw new Error(没有选择文件) if (file.size MAX_SIZE) { throw new Error(视频大小不能超过200MB) } if (!ACCEPT_TYPES.includes(file.type)) { throw new Error(视频格式不支持请上传MP4或WebM) } const newFileName ${Date.now()}_${(Math.random() * 10000).toFixed(0)}.${file.name.split(.).pop()} const formData new FormData() formData.append(file, new File([file], newFileName, { type: file.type })) const res await request.post(/upload/video, formData, { headers: { Content-Type: multipart/form-data }, timeout: 600000, onUploadProgress: onProgress }) return res.data.data.url }这个封装做了三件重要的事过滤文件、调整超时、重命名文件。其中重命名看起来多余实际上解决了文件原名可能包含中文字符和特殊字符导致的URL编码问题属于花钱买教训后总结出来的。6.2 组件里完整接入TinymceVue组件里我需要同时拿到视频URL、缩略图、上传进度。Tinymce的file_picker_callback只是一个回调函数不能直接触发Vue组件data更新所以不能像平时那样在回调里console一下就当完事。我用了临时变量和事件机制保证组件状态同步。template div editor v-modelcontent :initeditorInit inputhandleInput / div v-ifuploadProgress 0 uploadProgress 100 正在上传视频{{ uploadProgress }}% /div /div /template script import Editor from tinymce/tinymce-vue import { uploadVideo } from /utils/videoUpload export default { components: { Editor }, data() { return { content: , uploadProgress: 0 } }, computed: { editorInit() { return { plugins: media link image tables lists, toolbar: undo redo | styles | bold italic | bullist numlist | media image link | table, file_picker_callback: (callback, value, meta) { if (meta.filetype ! media) return const input document.createElement(input) input.type file input.accept video/* input.style.display none document.body.appendChild(input) input.click() input.onchange async () { const file input.files input.files[0] document.body.removeChild(input) if (!file) return try { const url await uploadVideo(file, (e) { if (e.total 0) { this.uploadProgress Math.round((e.loaded / e.total) * 100) } }) callback(url, { source: url, controls: true, width: 100% }) } catch (err) { console.error(err) this.$message.error(err.message) } finally { this.uploadProgress 0 } } } } } }, methods: { handleInput(val) { this.content val } } } /script这段代码里有一点要特别注意file_picker_callback内部的箭头函数继承了组件实例的this所以我可以在里面直接访问uploadProgress。如果换成普通function这一步就会出错。6.3 几个值得保留的边界处理第一个边界是连续多次打开媒体弹窗。我见过有同事在回调里每次都创建input但不移除页面里累计了一堆隐藏input导致第二次点击没反应。建议每次用完立即清除DOM元素。第二个边界是上传中用户关闭了弹窗。如果用户选择视频后上传还没结束就点了取消有可能出现关闭媒体窗口后进度条还挂在页面上。这个我是在finally里重置uploadProgress同时上传接口本身要支持中断axios可以通过AbortController实现组件销毁时cancel即可。第三个边界是编辑器内容isTrusted校验。有些情况下用户不点击文件输入框而是通过浏览器自动填写或拖拽触发input事件可能来自非用户交互。虽然没有严格必要但为了不让视频上传接口被机器人打建议在onchange里判断event.isTrusted至少记录下来。不过这个看业务部分后台系统需要允许管理员脚本自动上传这里就不做强制限制。7. 写在最后本地视频上传其实是个链路工程单看“Vue项目用Tinymce自定义本地上传视频”很多人以为只是配置一个回调实际上把文件校验、上传接口、回插参数、二次编辑、大文件超时这些问题全部理顺才算真正完成。我最大的体会是不要指望富文本编辑器帮你做资源管理它只负责把用户操作翻译成HTML。你现在如果也在做类似功能建议先拿一个小MP4文件跑通全链路再逐步加上大文件、进度条、封面。视频上传涉及的前后端配合点比图片多不要等联调时再临时补接口字段。最后分享一个小技巧Tinymce配置文件里可以给media插件设置media_filter_html为true这样从外部粘贴的视频HTML会被清洗反而容易丢属性。如果你正常上传没问题只是粘贴视频内容有问题先查这个参数被我当成bug排查了好几个小时。希望这篇文章能帮你少走这段弯路。

相关新闻

Attention-transfer:轻量化模型注意力迁移实战指南

Attention-transfer:轻量化模型注意力迁移实战指南

1. 项目概述:为什么“Attention-transfer”成了轻量化网络学习里绕不开的关键词最近在几个高校实验室和工业界模型压缩团队的交流中,反复听到一个词——“Attention-transfer”。它不像知识蒸馏(Knowledge Distillation)那样被写进…

2026/10/15 19:17:12 阅读更多 →
MySQL 基础 + AI SQL 优化

MySQL 基础 + AI SQL 优化

楠风测开 测试工程师复习日记 Day15 用一套订单、支付和库存样例,把查询、关联、金额核对与事务验证串起来。 前言:SQL跑通之后,还差一次结果验收 朋友们好,我是楠风。 上一阶段聊完Linux监控,我们开始补上测试工程…

2026/10/15 19:17:12 阅读更多 →
回文链表高效判断:快慢指针与反转链表实现O(1)空间解法

回文链表高效判断:快慢指针与反转链表实现O(1)空间解法

1. 题目与核心思路拆解1.1 回文链表到底在考什么回文链表这道题,在主流刷题平台的 Hot 100 榜单里长期占据前排位置。我第一次刷的时候,觉得很简单,链表的节点值放进数组,左右双指针一比对,不就完事了吗?后…

2026/10/15 19:17:12 阅读更多 →

最新新闻

云开发+短信API:零成本搭建员工生日自动提醒系统

云开发+短信API:零成本搭建员工生日自动提醒系统

每年一到员工生日这种日子,行政和HR就开始手忙脚乱。翻Excel、找手机号、挨个发祝福语,还要赶在当天发出,稍微一忙就会漏掉一两个。之前我们团队就是这么干的,直到有一次连续漏了三个同事的生日,被吐槽“公司没有人情味…

2026/10/15 20:02:24 阅读更多 →
Arcpy批量导出地图脚本:从手动出图到自动化流水线

Arcpy批量导出地图脚本:从手动出图到自动化流水线

1. 从手动一张张出图到批量流水线:这个脚本到底解决什么问题如果你做过GIS制图,大概率经历过这样的场景:手头有一份行政区划图层,领导要求按每个乡镇出一张专题图,一共几十上百个。打开ArcMap或ArcGIS Pro,…

2026/10/15 20:02:24 阅读更多 →
离散数学笔记高效整理法:五大板块与复习避坑指南

离散数学笔记高效整理法:五大板块与复习避坑指南

简介:这是一份系统梳理离散数学核心知识点的PDF笔记,围绕数理逻辑、集合论、数论、计数原理、概率论、关系论、代数系统与图论八大板块展开,适合高校本科生、考研复习者及程序员巩固理论基础。笔记从命题逻辑的永真式与范式讲起,依…

2026/10/15 20:02:24 阅读更多 →
python代码学习1

python代码学习1

摘要:f的用法、print中可用的操作、for、列表、ASCII编码一、f的用法:在输出里调整格式1.直接嵌入变量/表达式a 18 print(f"今年{a}岁") # 今年18岁2.保留有效数字pi 3.1415926 print(f"{pi:.2f}") # 3.14 print(f"{pi…

2026/10/15 20:02:24 阅读更多 →
IIS 6.0离线安装包:含全部DLL与注册表修复的SP2兼容部署方案

IIS 6.0离线安装包:含全部DLL与注册表修复的SP2兼容部署方案

简介:本资源为IIS 6.0完整安装支持包,面向Windows Server 2003系统管理员、传统Web运维人员及遗留系统维护开发者,专为解决IIS 6.0原生安装因缺失关键DLL导致失败或功能异常的典型问题。压缩包内含263个文件,涵盖19个核心DLL&…

2026/10/15 20:02:24 阅读更多 →
VMware复制粘贴失效?从VMware Tools到客户机隔离全排查指南

VMware复制粘贴失效?从VMware Tools到客户机隔离全排查指南

你是不是也遇到过这样的场景:虚拟机里装好了系统,想在宿主机和虚拟机之间复制一段代码、一个下载链接、一小段配置,结果CtrlC、CtrlV按了半天一点反应都没有;换成拖放文件,鼠标倒是能移过去,文件却死活拖不…

2026/10/15 20:01:22 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →