尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CKEditor5实战:视频引入、预览链路与自定义工具栏全解析

CKEditor5实战:视频引入、预览链路与自定义工具栏全解析 最近在做一套内容管理后台编辑器这块从零选型第一反应就是上 CKEditor5。原因也很直白项目里需要一个能自由插入视频、能在发布前直观预览效果、还能按不同角色收起或者扩展工具栏的富文本方案。CKEditor5 在这一轮对比中确实最合适——内置 Media Embed、自定义插件机制成熟、工具栏组件化程度高社区里能查到的配置案例也比其他编辑器多得多。这篇就把我落地过程的完整思路写出来包括视频引入的几种选择、预览链路里那些文件可能有害的警告是怎么来的以及自定义 toolabr 时真正该注意的细节。适合正在选型富文本编辑器、或者已经在 CKEditor5 里做二次开发的朋友参考。1. 为什么是 CKEditor5这次项目对编辑器的硬性要求1.1 业务场景里三个绕不开的需求我当时列的选型清单不算复杂但每一条都很致命。第一视频引入。运营同学需要在文章中间插入视频而且来源两种都有一种是腾讯视频、B站这类第三方平台的分享链接另一种是自己上传的 mp4 文件。整合进同一套内容里意味着编辑器既要能解析 iframe 嵌入又要能接收本地文件并转成可发布的资源地址。第二内容预览。编辑写完以后发布之前要在后台看到接近真实页面的效果。这不是简单地在旁边加一个 iframe 预览静态 HTML而是要保证编辑器数据解析出来的结果和线上渲染结果一致尤其是视频、图片这种块级元素不能在编辑器里是一套样子、发布出去变成另一套样子。第三自定义 toolbar。同一个编辑器在不同入口要呈现完全不同的操作集合。运营编辑用的入口要完整包括标题、列表、表格、视频、代码块审核人员的只读预览入口则不需要任何编辑按钮还有个别特定栏目连加粗斜体都不能出现只能插视频和写简介。这要求 toolbar 必须能在启动时动态生成而不是写死一份配置。另一个隐含需求是团队现有技术栈是 React Vite。CKEditor5 提供了对 React 友好的官方封装ckeditor/ckeditor5-react同时也能在纯 TypeScript 环境里直接以框架无关的方式调用迁移成本低。1.2 与上一代编辑器对比后的取舍团队之前用的是 CKEditor4坦白说功能上没什么不够用的但有两个痛点越来越明显一是视频插入基本靠写 HTML 源码编辑器内部对 video/iframe 的嵌套支持很弱。二是皮肤和按钮扩展非常依赖config.toolbar的模板套路一旦要加一个自定义按钮就得去翻旧插件体系的文档整体心智负担挺重。CKEditor5 不一样的地方在于它的架构做了彻底的模块化。编辑器实例本身是由一堆插件组合出来的toolbar只是 UI 层的配置入口背后每个按钮都对应一个 command每个 command 又由插件注册。这种设计让自定义工具栏变成一件非常自然的事——你不需要改框架源码只需要往插件集合里加自己的插件再把插件提供的按钮挂到 toolbar 配置项上。从数据层看CKEditor5 默认的模型是Content数据模型加上HtmlDataProcessor最终输出的 HTML 结构相对干净不会像旧版那样在源码模式下留下一堆nbsp;或者style。这对后续前端渲染和搜索结果摘要提取都友好很多。1.3 选择哪种编辑器构建方式CKEditor5 官网提供了三种落地方式直接引入现成构建包比如ckeditor5-build-classic用在线构建器挑选插件生成压缩包用 npm 包在自己工程里按需组装我选了第三种原因只有一个需要相对完整的可扩展性。用在线构建器虽然方便但每次想调一个插件版本或者自定义 schema就得重新去生成一次包代码仓库里也很难 review。按需组装虽然初始化代码写得长一点但好处是插件依赖清晰、版本可控出现问题可以直接定位到具体包。实际工程里我的最小依赖大致是这样的npm install ckeditor5 ckeditor/ckeditor5-react最近几个大版本ckeditor5包已经把常用功能整合到了一起不用再分别安装几十个ckeditor/ckeditor5-*小包。不过注意一些冷门插件仍然需要单独装比如后面要讲的 Media Embed。2. 视频引入的技术路线Media Embed、自定义上传与 iframe 形态2.1 先看清 CKEditor5 的视频方案地图CKEditor5 早期版本并没有一个独立的视频插件它把视频分成了两条路在线视频通过MediaEmbed插件解析第三方视频平台分享链接输出成 iframe本地视频早期版本需要社区插件新版本则可以用通用的video标签配合上传适配器实现如果你用的是ckeditor5-build-classic这种全量构建包里面默认是带MediaEmbed的。但如果你像我一样按需组装就需要手动安装npm install ckeditor/ckeditor5-media-embed然后在插件列表里注册import { MediaEmbed } from ckeditor5; import { MediaEmbedToolbar } from ckeditor5; ClassicEditor.create(document.querySelector(#editor), { plugins: [MediaEmbed, MediaEmbedToolbar], toolbar: [mediaEmbed, |, ...], mediaEmbed: { previewsInData: true } });previewsInData这个选项值得单独说。它决定生成的 HTML 数据里媒体内容是以一个简单的占位链接存在还是直接展开成完整的 iframe 预览结构。默认是false也就是编辑器里看到的是卡片样式但最终输出的 HTML 里只有原始 URL设置成true后HTML 里会直接输出 iframe这样在后台预览时就能看到真实视频画面。代价是数据体积变大——一个视频卡片会携带整段 iframe 代码。如果你需要兼容微信内置浏览器这类环境建议线上用previewsInData: false然后在前端渲染时自己根据 URL 动态生成对应的 iframe这样发布出去的内容更可控。2.2 给 Media Embed 扩展自定义视频平台第三方平台远不止 B 站、腾讯视频这些自带支持的类型。实际项目里可能还会遇到企业内部的视频点播平台或者某个不太主流的直播回放链接。CKEditor5 的 Media Embed 允许通过providers配置自定义 URL 解析规则mediaEmbed: { previewsInData: true, providers: [ { name: internal-video, url: /^https?:\/\/media\.example\.com\/video\/(.)/, html: (match) { const videoId match[1]; return iframe srchttps://media.example.com/player?vid${videoId} width100% height400 frameborder0 allowfullscreen/iframe; } } ] }url字段是一个正则表达式用来做匹配html字段接收一个函数返回拼接好的 iframe 代码。这样在编辑器里粘贴https://media.example.com/video/xxxx时CKEditor5 就会把它识别为一个媒体卡片并且按照你自己的规则渲染预览。这块有个容易踩的坑CKEditor5 默认的 provider 规则会优先匹配如果自定义规则排在后面而内置规则的正则又恰好能匹配你的链接那就会走内置逻辑。比较好的做法是把自定义 provider 放在数组最前面或者干脆用更严格的正则保证唯一匹配。2.3 本地视频上传用通用上传适配器接收 mp4本地视频引入本质上和图片上传路径一样都是通过editor.plugins.get(FileRepository)来处理。CKEditor5 把文件上传抽象成适配器模式常见的做法是自定义一个uploadAdapter把文件交给后端接口返回线上地址。我在这里直接把图片上传适配器复用了一份在此基础上扩展了文件类型判断让同一个适配器同时支持图片和视频。核心逻辑如下class UploadAdapter { constructor(loader, config) { this.loader loader; this.config config; } upload() { return this.loader.file.then((file) { const isVideo file.type.startsWith(video/); const endpoint isVideo ? this.config.videoEndpoint : this.config.imageEndpoint; return new Promise((resolve, reject) { const formData new FormData(); formData.append(file, file); fetch(endpoint, { method: POST, body: formData, }) .then((res) res.json()) .then((data) { if (data.url) { resolve({ default: data.url }); } else { reject(data.message || 上传失败); } }) .catch(reject); }); }); } abort() { // 终止上传逻辑 } }在编辑器初始化时通过editor.plugins.get(FileRepository).createUploadAdapter (loader) new UploadAdapter(loader, config)注入。但这里有一个非常重要的细节CKEditor5 默认的媒体嵌入命令并不能直接处理本地视频文件。如果你在工具栏上加了一个上传视频按钮然后直接调用execute(upload)你会发现视频并不会像图片那样被插入。正确的处理方式有两种第一种如果用的是较新版本可以直接在 schema 里声明支持video标签然后自己写一个插入命令。这种方式自由度最高但需要了解模型和视图的转换关系复杂度相对高一些。第二种也是我实际采用的方式是让后端在文件上传成功后返回一个能覆盖业务需求的媒体地址然后前端把这段地址包装成一个标准链接再触发 Media Embed 的解析命令。比如用户上传了一个 mp4后端返回/uploads/demo.mp4前端就把它组合成业务路由下的一个视频播放页链接再调用editor.execute(mediaEmbed, /play/xxx)CKEditor5 就会走已有的 Media Embed 逻辑输出 iframe。这样做的最大好处是不需要线性处理video标签的上下左右对齐、封面图、播放控件这些细节全部交给业务播放器页面去管。坏处是会有一次额外的页面跳转。如果产品接受这个交互实现成本会低非常多。3. “文件可能有害”提示背后预览链路的文件类型与 MIME 陷阱3.1 预览警告的真实来源项目中有一个让我卡了很久的问题用户在编辑器里插入一段在线视频后后台内容列表页的预览区域偶尔会弹出一条系统提示大意是你尝试预览的文件可能对你的计算机有害。如果你信任此文件以及其来源请打开此文件。一开始我以为是编辑器的问题后来排查发现根本不是。这条提示是操作系统层面的文件预览机制发出的常见于 Windows 资源管理器的预览窗格。当你在文件管理器中选中一个从网络驱动器、共享目录或者带Mark of the WebMOTW标记的文件时系统会检测到文件来源不可信于是跳出警告。视频文件尤其容易触发因为预览窗格要用系统播放器去解析它如果文件里还带了脚本或者奇怪的编码Windows 会在预览阶段就拦截。那么这和 CKEditor5 有什么关系关系在于我们后台的预览功能为了让运营快速查看文章效果直接把编辑器输出的 HTML 保存成了一个临时.html文件存到内网共享目录里然后在预览页面通过一个iframe去加载这个文件。这个文件在 Windows 看来就是来自其他计算机的文件于是预览窗格和浏览器在读取它的时候都会先警告。3.2 富文本编辑器嵌入文件时如何被误判更深一层的问题是即使不走共享目录编辑器本身也可能在预览阶段被安全策略卡住。比如你在mediaEmbed里配置了自定义 provider返回的 iframe 指向一个内网视频地址。如果内网视频地址的响应没有正确设置Content-Type浏览器会把响应体当作纯文本或者二进制流那在编辑器预览区域就只会看到一个空白框或者下载动作根本不会渲染视频。另一个常见场景是把本地 HTML 文件拖进编辑器去预览。CKEditor5 的HtmlDataProcessor对 HTML 文件内容进行解析但解析出来的 HTML 如果包含script标签编辑器模型默认会丢弃。这是 CKEditor5 自带的 XSS 防护机制本意是好的但会造成编辑器里看不到我拖进去的东西的困惑看起来像预览失败。所以当你看到无法预览之类的提示时可以先按这个思路排查当前预览的是文件系统的本地文件还是网络资源如果是本地文件是否带有 MOTW 标记如果是网络资源HTTP 响应头里的Content-Type是否正确目标地址是否允许被 iframe 嵌套加载3.3 我这边最终采用的预览方案经过两轮测试最终我把文章预览的链路改成了这样预览不再生成临时 HTML 文件放到文件目录而是通过一个内部接口动态渲染一个预览页该预览页的响应头强制设置Content-Type: text/html; charsetutf-8并加上X-Content-Type-Options: nosniff预览页里面嵌的视频地址统一走公司视频点播平台的 HTTPS 正式播放地址不再直接引用上传目录里的原始 mp4 文件预览页本身增加了白名单校验只允许后端配置的可信域名被 iframe 加载这样操作之后之前频繁出现的此文件可能有害提示基本消失了。本质上不是去关闭安全警告而是绕开了那些触发警告的场景。顺带一提如果只是给运营内部自用想要最省事的方案可以让后端直接把编辑器数据渲染成完整 HTML 字符串由前端window.open一个新窗口展示。这个窗口里的内容是后端拼接的不涉及本地文件自然就不会触发操作系统的文件来源检查。4. 自定义 toolbar 的实现从内置按钮到自己的功能4.1 先看清 toolbar 配置的数据结构CKEditor5 的 toolbar 配置看起来就是一组字符串但实际上背后有一套分组逻辑。一个按钮项可以是字符段比如bold对应一个按钮分隔符|是竖线分隔符-是行分隔符子数组用来把多个按钮包进一个下拉组我项目里的最小配置大概是这样的toolbar: { items: [ heading, |, bold, italic, link, bulletedList, numberedList, |, mediaEmbed, insertVideo, |, undo, redo ] }有一点容易被忽略很多构建包默认带的 toolbar 配置比这个长得多如果你手动指定toolbar.items一定要确保被引用的每个按钮名都有对应插件在plugins数组里。否则编辑器启动时会直接报错提示找不到某个组件。4.2 动态生成 toolbar同一套编辑器适配多个角色文章开头我提过需要根据不同入口控制编辑能力。这个需求本质上是在初始化时根据条件拼装 toolbar 数组。我的做法是写了一个函数根据当前用户角色返回不同的配置function buildToolbar(role: string): string[] { const base [undo, redo, |, heading, |]; const editingTools [bold, italic, link, bulletedList, numberedList]; const mediaTools [mediaEmbed, insertVideo]; if (role admin) { return [...base, ...editingTools, |, ...mediaTools]; } if (role editor) { return [...base, ...editingTools]; } if (role reviewer) { return []; } return [...base, ...mediaTools]; }reviewer 的工具栏是空数组这并不代表编辑器只能只读。CKEditor5 里真正的只读模式是通过editor.isReadOnly控制的而不是靠隐藏按钮。空数组只表示 UI 上不显示任何操作按钮但你仍然可以用 API 修改内容。所以审核预览场景我是直接把编辑器实例设置为只读editor.isReadOnly true;4.3 给工具栏添加一个真正的自定义按钮Media Embed 自带的mediaEmbed按钮只能针对当前选区操作如果用户想在不输入任何文字的情况下直接点一个按钮来弹窗上传本地视频就需要注册自己的组件。自定义按钮的官方做法是写一个插件插件里向editor.ui.componentFactory注册一个新的按钮名然后在工具栏配置里引用这个名称。下面是精简后的代码import { Plugin } from ckeditor5; import { ButtonView } from ckeditor5; class InsertVideoButton extends Plugin { init() { const editor this.editor; editor.ui.componentFactory.add(insertVideo, (locale) { const button new ButtonView(locale); button.set({ label: 插入视频, icon: videoIcon, // 这里需要自己准备一个 SVG 图标 tooltip: true }); // 点击按钮后触发一个自定义命令 this.listenTo(button, execute, () { editor.execute(insertVideoFromUrl); editor.editing.view.focus(); }); return button; }); } }为了让execute(insertVideoFromUrl)能正常工作还需要注册一个 command。命令可以直接扩展Command类内部调用媒体嵌入逻辑import { Command } from ckeditor5; class InsertVideoFromUrlCommand extends Command { execute(url: string) { const editor this.editor; const selection editor.model.document.selection; editor.model.change((writer) { // 在光标位置插入一个 media 元素 const mediaElement writer.createElement(media, { url: url }); editor.model.insertContent(mediaElement, selection); }); } refresh() { // 可以在这里根据选择状态控制按钮的可用性 this.isEnabled true; } }命令写好后要在插件初始化时把它绑定到编辑器命令表上editor.commands.add(insertVideoFromUrl, new InsertVideoFromUrlCommand(editor));这样整套链路就通了工具栏按钮 → 命令 → 模型插入 → 视图渲染通过 Media Embed 的转换器→ 生成 iframe 预览。4.4 自定义按钮图标与 UI 细节处理用在线构建器或全量构建包时内置按钮是不需要额外打包图标的。但自定义按钮必须要自己传一个 SVG 字符串或者 URL。比较省事的方式是从 CKEditor5 源码仓库的ckeditor5-icons里挑一个图标或者直接用设计稿里现成的 SVG。另一个实际问题是按钮在窄屏下的表现。编辑器工具栏在移动端默认会收起到一个More下拉里但前提是 UI 层检测到空间不足。如果你的后台是桌面端为主通常会希望工具栏可以换行而不是收起。配置方式toolbar: { shouldNotGroupWhenFull: true }设置为true后工具栏按钮超过宽度限制时会自动换行不会折叠到下拉菜单里。这对内容编辑场景反而更友好因为用户一眼能看到所有可用功能。5. 线上遇到过的真实坑版本差异、缓存与兼容性5.1 版本升级带来的配置不兼容CKEditor5 版本更新比较激进大版本之间经常会出现配置项改名、插件拆分的情况。我在项目中途从 v36 升到 v38 时就遇到过两个问题。第一个是mediaEmbed相关的包名变了。早期版本里有一个ckeditor/ckeditor5-embed到了新版本合并进了ckeditor5主包里的MediaEmbed。如果你按照老文档安装会出现模块不存在的报错。第二个是Alignment插件的 toolbar item 命名从alignment变成了具体按钮比如alignLeft、alignCenter。如果你在旧项目里配置了toolbar: [alignment]升级之后按钮不会显示而且没有任何 warning比较坑。建议升级后做一个 Smoke Test检查所有自定义按钮是否还挂在工具栏上、Media Embed 能不能正常解析现有数据里的链接。5.2 按钮有图标但点不动时的排查链路遇到次数最多的诡异问题是自定义按钮正常显示图标也在但点击没有任何反应。这种情况基本都发生在命令还没注册成功或者componentFactory里返回的按钮实例没有正确listenTo点击事件。我的排查顺序一般是这样打开控制台输入editor.commands.get(insertVideoFromUrl)看返回结果是 command 实例还是 undefined如果 command 是 undefined说明插件初始化里editor.commands.add没有执行或者插件的init()方法根本没跑检查编辑器实例上是否存在editor.plugins.get(InsertVideoButton)如果不存在查看 plugins 数组里有没有正确传入自定义插件类如果 command 存在再检查按钮点击监听有没有被注册可以在execute回调里加一个console.log验证还有一种非常隐蔽的情况多个编辑器实例共用同一个 DOM 容器前一个实例销毁后没有完全清空事件监听导致第二个实例的按钮 execute 触发后被前一个实例悄悄拦截。这种问题定位时要靠浏览器开发者工具里的元素面板检查当前容器绑定的是哪个实例的 view。5.3 视频预览的跨浏览器差异不同浏览器对 iframe 预览的处理策略差别不小。Chrome 对无allowfullscreen的 iframe 会默认禁用全屏按钮Firefox 则不会主动拦截Edge 在加载包含内部分辨率不明确的视频时会先显示一个黑框。这让预览效果在不同电脑上看起来完全不同。我的应对策略是在mediaEmbed.providers的自定义 HTML 模板中强制把 iframe 宽度设置为容器百分比而不是固定像素值给 iframe 统一加上allowfullscreen和allowautoplay; fullscreen; picture-in-picture在编辑器初始化完成后通过editor.editing.view.change遍历所有 iframe如果发现缺失allowfullscreen属性就动态补上最后还有一个来自同事反馈的兼容性问题部分用户在用 IE 内核的办公环境下打开后台CKEditor5 直接白屏。CKEditor5 官方从 v27 开始就不再支持 IE11 了这个无解只能在前端入口做浏览器检测提示用户切换到 Chromium 内核浏览器。工具选型和落地过程中最深的体会是CKEditor5 的文档虽然全面但很多细节需要结合业务场景才能理解为什么这么设计。Media Embed 和自定义 toolbar 只是它能力的一部分真正值得花时间的其实是去弄懂插件、命令、schema 这三者之间的关系。你把这条链路理清楚了后面不管加视频还是加音频或者做自定义表格功能思路都会顺很多。
返回列表