news 2026/10/5 9:53:59

CKEditor5视频引入、实时预览与自定义Toolbar插件开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CKEditor5视频引入、实时预览与自定义Toolbar插件开发实战

這陣子又跟 CKEditor5 纏上了。客戶的後台管理系統需要一個富文本編輯器,規格表上寫得明明白白:要有影片引入、編輯當下就要能立即預覽、工具列還不能是預設那一大串,必須支援自訂 toolbar。如果你也在查「CKEditor5 要怎麼插入影片」或「toolbar 按鈕為什麼少一個」,這篇就是為你寫的。我會把整個思考過程從元件選型、外掛開發、上傳與後端收稿,一路講到常見坑,盡量讓你可以直接照著抄。

這個題目在簡體社群常被寫成「CKEditor5富文本编辑器 - 视频引入、预览及自定义toolbar」,我用繁體中文把流程重新整理一遍。影片就是 video,預覽就是 preview,自訂就是 custom,toolbar 就是工具列。不管你是前端、全端、還是維護老後台的人,只要專案裡有「文章編輯器」,這篇應該都幫得上。

1. 專案概述與需求拆解

1.1 這個專案到底在解決什麼問題

先講需求場景。客戶希望後台管理員撰寫公告、教學文章、活動頁面時,可以直接把一段示範影片放進內文,而不是只給一個超連結。管理員多半不懂 HTML,他們要的是「按一個按鈕、貼上網址或選一個檔案、影片就出現在文章裡」,而且編輯當下就要能看到畫面。儲存之後,前台網站也要用同樣的影片內容正常播放。

另一個需求是工具列瘦身。CKEditor5 預設的 ClassicEditor 工具列擺了十幾個按鈕,但客戶實際只用到標題、粗體、清單、插入影片、原始碼模式,其他按鈕只會造成誤按。因此必須做成「依照後台角色或頁面類型,決定顯示哪些工具列按鈕」的彈性架構。

拆解下來,核心需求有三個:

  • 影片引入:可以透過網址嵌入,也可以上傳影片檔到伺服器,然後自動插入文章。
  • 即時預覽:編輯器內就能看到<video>標籤的播放器,儲存前可以先確認內容長相。
  • 自訂 toolbar:不是用預設一大串,而是依照需求動態組出工具列,包含自訂的影片按鈕。

這三個需求單獨看都不難,但實際上很多人會在「影片」這一步卡住。原因很簡單:CKEditor5 官方 build 預設只有圖片處理,沒有大家想像中的「影片按鈕」。要支援影片,必須自己補上 schema、conversion、command、button,才能讓整個流程順暢運作。

1.2 為什麼選擇 CKEditor5

市面上富文本編輯器很多,常見的還有 Quill、TinyMCE、Slate 之類。我在這個專案選 CKEditor5,不是因為它最簡單,而是因為它的擴充架構相對乾淨,也提供了完整的文件與範例。

CKEditor5 是 CKEditor 4 之後的大改版,底層改成 model-view-controller 架構。簡單說,編輯器內看到的是「view」,實際儲存的資料是「model」,兩者之間靠 conversion 機制互相轉換。你可以把 model 想成資料庫裡的一筆結構化資料,view 想成瀏覽器畫面上看到的 HTML。好處是輸出乾淨、格式統一,壞處是學習門檻比 Quill 高一點,尤其當你要自訂一個全新的元素時,得先理解 schema 與 conversion。

如果你是團隊裡唯一負責編輯器整合的人,我建議不要急著直接把網路上的程式碼複製貼上。先花半小時讀懂 CKEditor5 官方的「Custom plugins」教學,後面寫起程式碼會順很多。這篇也是預設你已經能安裝、能跑起一個最基本的 CKEditor5 編輯器。

2. 整體設計:影片引入、預覽與工具列的底層邏輯

2.1 CKEditor5 的架構與外掛機制

CKEditor5 整個編輯器由一堆 Plugin 組成。Typing、Paragraph、Heading、Bold 都是 Plugin。Plugin 可以做三件事:註冊 schema、定義 conversion、註冊 command 與 UI 元件。

當你呼叫ClassicEditor.create(el, config)時,config 裡的plugins陣列決定載入哪些外掛。每個外掛都可以在init()裡把事情掛進編輯器。例如:

  • editor.model.schema.register():定義「影片元素」長什麼樣子,可以有哪些屬性、能不能放在段落中間。
  • editor.conversion.for('downcast'):定義 model 轉成 view 的規則,也就是編輯畫面上要顯示成什麼 HTML。
  • editor.conversion.for('upcast'):定義 view 轉成 model 的規則,也就是讀取既有 HTML 時,怎麼把<video>認成編輯器內的影片元素。
  • editor.commands.add():註冊一個指令,例如「插入影片」。
  • editor.ui.componentFactory.add():註冊一個工具列按鈕。

所以「插入影片」這件事,本質上是「新增一個自訂元素」。只要 schema 允許、conversion 有寫好,後端拿到editor.getData()就會輸出完整的<video>標籤。

2.2 影片引入的三種路徑與選型考量

在規劃階段,我整理過三種做法,這裡直接給比較表:

方案適合情境優點缺點
官方 MediaEmbed只需要嵌入 YouTube、Vimeo 等外部影音平台設定簡單,幾乎零程式碼不支援直接放 mp4 影片檔
General HTML Support + SourceEditing後台使用者懂 HTML,願意自己貼語法彈性最大,任何 HTML 都能放行一般管理員不會用,且有 XSS 風險
自訂 VideoPlugin後台使用者只會按按鈕,需要上傳與預覽使用體驗最好,可完全掌控要自己寫 schema、conversion、command

我的結論是:如果客戶只是要貼 YouTube 影片,用官方 MediaEmbed 就夠了。但如果需求是「上傳一支 mp4,直接在文章內播放」,那最終還是得走第三種,自訂 VideoPlugin。實務上我也會同時安裝 MediaEmbed,讓管理員可以另外嵌入 YouTube 或 Vimeo,兩者並不衝突。

如果你想要快速驗證,可以先用第二種方案:開啟 General HTML Support 與 SourceEditing,然後在原始碼模式下貼一段<video controls src="xxx.mp4"></video>。這樣十秒就能看到影片在編輯器內顯示,但缺點是對一般使用者不友善。我的正式做法是第三種,下面會完整示範。

2.3 預覽的本質

很多人以為「預覽」是 CKEditor5 特別提供的功能,其實不是。當你把<video>標籤放進編輯器,編輯器本身在 editing view 就是一個普通的<video>元素,瀏覽器原生就支援影片播放。所以你不需要寫任何預覽邏輯,只要 conversion 正確,編輯當下就能看到播放器。

真正要注意的是「前台預覽」與「安全性」的問題。文章儲存後,你可能會在另一個管理頁面用innerHTML把editor.getData()的內容塞進畫面,讓管理員預覽整篇文章。這樣做雖然方便,但如果文章內容可能混入惡意 HTML,就有 XSS 風險。比較安全的做法是用 sandbox 屬性的 iframe 來隔離,或先經過 DOMPurify 之類的消毒工具。

另外,有些網頁會跳出「你嘗試預覽的文件可能對你的計算機有害」的警語,這通常是 Windows 檔案總管對從網路下載的檔案加了 Mark-of-the-Web(MOTW)造成的,跟編輯器本身無關。我在第五節會再補一段實際處理方式。

2.4 自訂 Toolbar 的規則與注意事項

CKEditor5 的工具列設定很直覺,在 config 中指定toolbar.items即可。舉例:

toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'bulletedList', 'numberedList', '|', 'insertVideo', 'sourceEditing' ] }

|是群組分隔線,-則會讓後面的按鈕換行。這裡有個容易踩的坑:insertVideo這個名字必須跟componentFactory.add('insertVideo', ...)註冊的名稱完全一樣,否則按鈕不會出現。如果你發現工具列少了一個按鈕,第一件事去看editor.ui.componentFactory.names(),它會列出所有已註冊的元件名稱,比對一下就知道了。

另外要注意,CKEditor5 的工具列可以動態調整。有些人會依照登入者角色帶入不同toolbar.items,例如管理員有 SourceEditing 按鈕、一般編輯者沒有。這在實務上很常見。你可以先建立一個按鈕列表,根據角色 filter 後再丟給 config,不需要寫多餘的 UI 邏輯。

3. 實操:從零打造一個可上傳、可預覽影片的 CKEditor5 元件

3.1 環境準備與安裝

這個專案我用的是 npm 安裝方式。如果你使用打包工具,例如 Webpack 或 Vite,可以直接安裝 ClassicEditor build:

npm install @ckeditor/ckeditor5-build-classic

但因為我們要自訂 Plugin,建議改用「線上 Builder」或直接安裝分散的套件。線上 Builder(CKEditor 5 Builder)可以在網頁上勾選你要的功能,下載一個客製化包,包裡面已經帶好所有相依套件。如果你希望完全掌控版本,也可以手動安裝:

npm install --save @ckeditor/ckeditor5-core npm install --save @ckeditor/ckeditor5-ui npm install --save @ckeditor/ckeditor5-engine npm install --save @ckeditor/ckeditor5-build-classic npm install --save @ckeditor/ckeditor5-html-support npm install --save @ckeditor/ckeditor5-source-editing

實際打包時,我只在頁面引入 ClassicEditor 的建置檔與樣式,另外把自訂 Plugin 寫在獨立檔案,方便日後維護。

3.2 設定 General HTML Support 放行 video 標籤

如果你只是想先測測看「HTML 預覽」這件事,最快的方法是開啟GeneralHtmlSupport,然後在htmlSupport設定allow。範例如下:

ClassicEditor.create(document.querySelector('#editor'), { plugins: [ // 其他 plugin GeneralHtmlSupport, SourceEditing ], htmlSupport: { allow: [ { name: 'video', attributes: ['src', 'controls', 'autoplay', 'loop', 'muted', 'poster', 'width', 'height', 'style'] }, { name: 'source', attributes: ['src', 'type'] } ] } });

這樣設定之後,你在原始碼模式貼<video controls src="https://example.com/video.mp4"></video>,編輯器內就會出現播放器。這個做法適合快速驗證,但我不建議只在這個方案就上線,因為一般後台使用者不會想開原始碼模式貼 HTML。

3.3 自訂 VideoPlugin:註冊按鈕、開啟對話框、插入影片標籤

正式做法是寫一個 VideoPlugin。我先把最核心的程式碼完整貼出來,接著逐步解釋。

// InsertVideoCommand.js import { Command } from '@ckeditor/ckeditor5-core'; export default class InsertVideoCommand extends Command { execute(videoUrl) { const editor = this.editor; const model = editor.model; model.change(writer => { const videoElement = writer.createElement('video', { src: videoUrl, controls: 'controls', style: 'max-width: 100%;' }); const insertPosition = model.document.selection.getFirstPosition(); model.insertContent(videoElement, insertPosition); }); } }

這裡的writer.createElement('video', ...)會建立一個 model 節點,名字叫video。你可能會問:「為什麼是 video?CKEditor5 預設沒有這個元素啊?」沒有錯,所以我們要在 Plugin 裡先跟編輯器說明 video 元素長什麼樣,這就是 schema 的工作。

// VideoPlugin.js import { Plugin } from '@ckeditor/ckeditor5-core'; import { ButtonView } from '@ckeditor/ckeditor5-ui'; import InsertVideoCommand from './InsertVideoCommand'; export default class VideoPlugin extends Plugin { static get pluginName() { return 'VideoPlugin'; } init() { const editor = this.editor; editor.model.schema.register('video', { isObject: true, allowWhere: '$block', allowAttributes: ['src', 'controls', 'poster', 'style'] }); editor.conversion.for('upcast').elementToElement({ view: { name: 'video' }, model: (viewElement, { writer }) => { return writer.createElement('video', { src: viewElement.getAttribute('src'), controls: viewElement.hasAttribute('controls') ? 'controls' : undefined, poster: viewElement.getAttribute('poster'), style: viewElement.getAttribute('style') }); } }); editor.conversion.for('downcast').elementToElement({ model: 'video', view: (modelElement, { writer }) => { return writer.createContainerElement('video', { src: modelElement.getAttribute('src'), controls: modelElement.getAttribute('controls'), poster: modelElement.getAttribute('poster'), style: modelElement.getAttribute('style') }); } }); editor.commands.add('insertVideo', new InsertVideoCommand(editor)); editor.ui.componentFactory.add('insertVideo', locale => { const button = new ButtonView(locale); button.set({ label: '插入影片', icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M2 4h16v12H2z" fill="none" stroke="currentColor"/><polygon points="8,7 14,10 8,13" fill="currentColor"/></svg>', tooltip: true }); button.on('execute', () => { const url = window.prompt('請輸入影片網址(mp4、webm 或外部影片頁面網址)'); if (url) { editor.execute('insertVideo', url); } }); return button; }); } }

這段程式碼做了四件事:

  • schema.register:定義 video 元素是 block object,允許放在段落之間,並允許特定屬性。
  • upcast:把從資料來源讀進來的<video>轉成 model 節點。
  • downcast:把 model 節點轉回<video>,並帶上屬性。
  • componentFactory.add:新增一個工具列按鈕,點擊後用window.prompt請使用者輸入網址,再執行insertVideo指令。

我用了window.prompt是為了讓範例最少化,實際產品我建議改成自訂 modal,讓使用者可以輸入網址或上傳檔案。底層邏輯都一樣,只是 UI 比較友善。

3.4 將自訂按鈕掛進 Toolbar

有了 Plugin 之後,要記得在 config 中載入並把按鈕放進 toolbar。

import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import VideoPlugin from './VideoPlugin'; ClassicEditor .create(document.querySelector('#editor'), { plugins: [ VideoPlugin, // 其他官方 plugin ], toolbar: { items: [ 'undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'bulletedList', 'numberedList', '|', 'insertVideo' ] } }) .then(editor => { window.editor = editor; }) .catch(error => { console.error(error); });

如果一切正常,工具列最後面會出現一個「插入影片」的按鈕。點下去跳出輸入框,輸入https://example.com/video.mp4,編輯器內就會出現可播放的影片。

這裡有一個小技巧:如果你使用@ckeditor/ckeditor5-build-classic這個建置包,config 的plugins陣列要特別注意。有些版本的 ClassicEditor 已經內建一堆 plugin,你把plugins重新指定後,可能會覆蓋掉原本預設的 plugin。最保險的方法是用ClassicEditor.builtinPlugins展開,再加上自訂 Plugin:

plugins: [ ...ClassicEditor.builtinPlugins, VideoPlugin ]

這樣可以避免「按鈕都進 toolbar 了,但編輯器功能壞掉」的奇怪問題。

3.5 前端預覽與後端收稿的配合

儲存時,我們會用editor.getData()取得 HTML 字串,這個字串會包含<video>標籤。如果你的後端原本有自己的 HTML 過濾器,請先確認白名單有沒有放行 video 標籤,否則影片標籤很可能會被默默刪掉。

前台預覽文章時,如果是管理員介面,我建議用 iframe sandbox:

<iframe sandbox="" srcdoc="<base target='_blank'>${html}"></iframe>

sandbox 可以限制執行 JavaScript,至少不會讓內文裡可能的<script>直接在管理員的後台頁面執行。當然,最根本的防線還是後端要 sanitize。

3.6 上傳影片的設計

很多需求不只是貼網址,而是「上傳一支影片到伺服器,然後插入文章」。這部分跟一般檔案上傳一樣,我會在按鈕的 execute 裡建立一個隱藏的<input type="file">,讓使用者選檔後用FormData送到後端。

button.on('execute', () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'video/*'; input.onchange = async () => { const file = input.files[0]; if (!file) return; const formData = new FormData(); formData.append('file', file); try { const response = await fetch('/api/upload/video', { method: 'POST', body: formData }); if (!response.ok) { throw new Error('上傳失敗'); } const data = await response.json(); editor.execute('insertVideo', data.url); } catch (error) { console.error(error); alert('影片上傳失敗,請確認檔案格式與大小'); } }; input.click(); });

這裡要注意的是,accept="video/*"只是前端提示,後端還是要檢查真實的 MIME 類型與副檔名。影片檔案通常很大,伺服器如果沒有調大上限,上傳到一半就會被擋掉。

4. 上傳背後的檔案處理與安全底線

4.1 影片上傳介面怎麼設計

後台管理員通常不是技術人員,上傳介面要越簡單越好。我自己的建議是:

  • 支援拖曳上傳或檔案選取。
  • 上傳時顯示進度條,至少要有「處理中」的狀態。
  • 上傳完成後先回傳一個臨時 URL,讓使用者在編輯器內看到預覽。
  • 如果伺服器轉檔時間較長,用非同步工作處理,不要讓 HTTP 請求卡住。

影片格式方面,瀏覽器原生播放最保險的格式是 MP4(H.264 + AAC)與 WebM。假如你允許使用者上傳.avi或.mkv,Chrome 或 Safari 不一定能直接播放,後續就要做轉檔,這會讓專案複雜很多。客戶如果只是內部後台要放影片,我會在後端直接把非 MP4 檔轉成 MP4,確保前台一定可以播放。

4.2 後端接收與檔案大小限制

後端我用 Node.js 搭配 Multer 做範例:

const multer = require('multer'); const upload = multer({ storage: multer.diskStorage({ destination: 'uploads/', filename: (req, file, cb) => { const ext = path.extname(file.originalname); cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`); } }), limits: { fileSize: 200 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('video/')) { cb(null, true); } else { cb(new Error('請上傳影片檔案')); } } }); app.post('/api/upload/video', upload.single('file'), (req, res) => { const url = `/uploads/${req.file.filename}`; res.json({ url }); });

如果前面有 Nginx,記得調整client_max_body_size,例如:

client_max_body_size 200m;

如果後端是 PHP,則要檢查upload_max_filesize與post_max_size。很多「上傳到一半失敗」的問題,其實根本不是程式有 bug,而是伺服器設定擋掉了。

4.3 儲存與輸出的衛生問題:sanitize 是必須的

我一直強調 sanitize,因為富文本編輯器的源頭就是讓使用者輸入 HTML,如果後端完全信任這串 HTML,等於把系統的大門打開。CKEditor5 本身會過濾掉它不認識的元素,但自制 Plugin 既然允許<video>標籤,就要小心攻擊者塞入其他屬性,例如:

<video src="https://example.com/x.mp4" onerror="alert(document.cookie)"></video>

瀏覽器在載入影片失敗時會觸發onerror,如果你的 sanitize 規則只擋script沒擋事件屬性,XSS 就成立了。正確做法是白名單,只允許必要的屬性:

<video src="..." controls poster="..." style="max-width:100%;"></video>

後端可以用sanitize-html或DOMPurify設定白名單,例如:

const sanitizeHtml = require('sanitize-html'); const clean = sanitizeHtml(dirtyHtml, { allowedTags: ['p', 'br', 'strong', 'em', 'h2', 'h3', 'ul', 'ol', 'li', 'video'], allowedAttributes: { video: ['src', 'controls', 'poster', 'style'] }, allowedStyles: { video: { 'max-width': [/^\d+(px|%)$/] } } });

style屬性也要限制,否則 CSS 可能造成版面破壞,甚至使用background: url(...)做奇怪的事情。安全這件事,寧可一開始就嚴格,也不要出事之後再補洞。

4.4 繞開 XSS 的正確姿勢

除了後端 sanitize,前端也應該對editor.getData()的輸出做處理。我的標準流程是:

  1. 後端收稿後立即 sanitize。
  2. 儲存前把base href換成自己的網域,避免相對路徑被改成惡意網址。
  3. 前台渲染時,如果只是預覽,優先使用 iframe sandbox。
  4. 真正要直接嵌進頁面時,再允許受信任的影片標籤,其餘一律轉成純文字。

不要依賴瀏覽器或編輯器自動擋掉 XSS,因為編輯器只是編輯介面,不是安全邊界。只要有「使用者可以輸入 HTML」的地方,後端驗證永遠是主戰場。

5. 常見問題與排錯技巧實錄

5.1 影片標籤被過濾器吞掉

我遇過最常見的狀況:編輯器內明明看得到影片,存檔後再打開,<video>不見了。第一時間不要懷疑 CKEditor5,先去後端檢查資料庫存的是什麼。如果後端有套 HTML 過濾器或 WYSIWYG 元件庫,它們很可能預設不允許 video 標籤,直接就把整段刪掉。

排查方法很簡單:

  • 在瀏覽器 console 輸入editor.getData(),看輸出有沒有包含<video>。
  • 有包含:問題在後端,檢查 sanitize 白名單。
  • 沒包含:問題在前端 schema 或 conversion,檢查 upcast / downcast 是否註冊成功。

5.2 編輯器內一片空白或無法預覽

編輯器內沒畫面,最常見的原因有:

  • downcast沒註冊,model 存在但 view 不知道要顯示成什麼。
  • 影片網址是空白或錯誤,瀏覽器無法載入。
  • 影片格式是 Safari 不吃的 WebM。
  • 頁面是 HTTPS,但影片網址是 HTTP,瀏覽器擋掉混合內容。

混合內容的解法很簡單:把所有影片網址統一改成 https,或使用同源的相對路徑。後端在儲存時也可以強制把http://轉成https://,避免管理員貼了奇怪的網址。

5.3 上傳失敗與請求被擋

上傳大檔失敗,先看瀏覽器的 Network 面板,確認請求有沒有真的送到後端。常見原因:

  • Nginxclient_max_body_size太小。
  • PHPupload_max_filesize太小。
  • Node.js 的 body parser 沒設定limit。
  • 後端 timeout 太短,大型影片上傳時間超過伺服器給的請求上限。

我習慣先準備一支小檔案測試,確認小檔能過之後,再逐步加大,比較容易抓出是哪一層卡住。

5.4 工具列按鈕莫名消失

按鈕沒出現,十之八九不是 CSS 問題,而是註冊流程有誤。檢查順序如下:

  1. Plugin 有沒有放進plugins陣列。
  2. componentFactory.add('insertVideo', ...)裡的名字,跟toolbar.items裡的名字是否完全一致。
  3. 有沒有在建立編輯器時把過多的 CSS reset 影響到按鈕顯示。
  4. 如果使用ClassicEditor.builtinPlugins展開,自訂 Plugin 有沒有放在後方覆蓋同名元件。

也可以直接在 console 執行:

editor.ui.componentFactory.names()

這個方法會列出目前所有已註冊的 UI 元件,是排錯最快的方式。

5.5 作業系統或瀏覽器跳出「檔案可能有害」之類的警語

如果你在使用者點擊預覽或下載影片時,系統跳出「你嘗試預覽的文件可能對你的計算機有害。如果你信任此文件以及其來源,請打開此文件」的字樣,這通常不是你的網站被入侵,而是 Windows 對從網路下載的檔案加了 MOTW(Mark-of-the-Web)標記。瀏覽器下載的檔案會被標記為「來自網際網路」,Windows 檔案總管的預覽窗格會因此跳出警告。

處理方式要看情境:

  • 如果是後台管理員要預覽伺服器上的影片,最直接的做法是讓管理員經由你網站的播放頁面觀看,不要用檔案總管預覽。
  • 如果是使用者下載後在檔案總管打不開,可以在檔案上按右鍵,選擇「內容」,勾選「解除封鎖」,或使用 PowerShell 指令Unblock-File -Path 檔案路徑。
  • 如果只是想要讓 Windows 預覽窗格不要一直跳警告,可以關閉預覽窗格,或改用自動播放器檢視。

老實說,這個問題跟 CKEditor5 沒有直接關係,但因為影片上傳常常伴隨著「預覽」環節,很多人在整合時會一起碰到,我就把它列進來了。

5.6 如果你想預覽的是 Office 或 PDF(OnlyOffice/AList 延伸)

如果你把富文本編輯器用在知識庫或檔案管理系統,可能不只要在文章內播放影片,還希望管理員可以直接預覽.pdf、.docx、.xlsx這類文件。這條路跟「影片預覽」截然不同,因為瀏覽器原生不支援 docx 或 xlsx 的內容渲染。

常見做法是用 Docker 部署 OnlyOffice Document Server,再搭配 AList 或自己的檔案服務來做檔案列表與預覽入口。架構大概是:

  • OnlyOffice Document Server 負責把 Office 文件轉成可檢視的格式,或是在 iframe 中提供完整的編輯與預覽介面。
  • AList 負責串接儲存空間,提供檔案下載或預覽 URL。
  • 前端可以產出一個「在 OnlyOffice 中開啟」的按鈕,透過 iframe 嵌入預覽頁面。

如果你目前的專案剛好是「後台文章編輯器 + 上傳附件」,附件如果是 PDF,其實也可以用<iframe src="xxx.pdf">讓瀏覽器直接預覽。Office 文件就比較麻煩,需要 OnlyOffice 或微軟的 Office Online Viewer 之類的服務。不過這屬於另一個專案範疇了,有需要再單獨拉一篇講。

5.7 排查順序與除錯 SOP

最後我整理一個自己的除錯順序,遇到問題時照著跑,通常可以在五分鐘內定位:

步驟動作檢查重點
1看瀏覽器 console是否有 JavaScript 錯誤
2看 Network 面板是否有請求失敗、上傳中斷
3執行editor.getData()確認 model 輸出是否正確
4比對 schema 註冊editor.model.schema是否有 video
5比對 conversion上傳後與取得資料時,HTML 是否來回一致
6檢查後端 sanitize白名單是否放行 video 與必要屬性
7檢查伺服器靜態檔案影片 URL 是否能直接存取、MIME 是否正確

6. 個人實作體會與幾個可以再往下玩的方向

6.1 我推薦的設定組合

做了幾個專案之後,我現在遇到「富文本編輯器 + 影片需求」時,預設會用這個組合:

  • ClassicEditor 為基底。
  • 官方 Essentials、Paragraph、Heading、List、Bold、Italic。
  • SourceEditing:讓進階使用者能手動修 HTML。
  • MediaEmbed:讓使用者可以直接貼 YouTube 或 Vimeo 連結。
  • 自訂 VideoPlugin:提供上傳或輸入超過影音平台範圍的影片。
  • 後端 sanitize 白名單:放行p、h2、h3、ul、ol、li、a、img、video、source、figure、figcaption等必要元素。

工具列我則會依角色拆成兩份,例如:

const commonToolbar = ['undo', 'redo', '|', 'heading', '|', 'bold', 'italic', '|', 'bulletedList', 'numberedList']; const adminToolbar = [...commonToolbar, '|', 'sourceEditing']; const editorToolbar = [...commonToolbar, '|', 'insertVideo'];

這樣做比較好維護,之後同事要加按鈕,只要改陣列就好。

6.2 實務上最常踩的坑

除了前面講的 sanitize、上傳大小、componentFactory 名稱之外,還有幾個容易被忽略的小地方。

第一個是 React 或 Vue 的 StrictMode。某些前端框架在開發模式下會把元件掛載兩次,如果你的 CKEditor5 初始化程式碼寫得不夠嚴謹,編輯器可能建立兩次,造成「奇怪的按鈕重複」或「toolbar 錯亂」。我自己習慣在元件銷毀時確實呼叫editor.destroy()。

第二個是編輯器內容的「換行」問題。vlack 元素在 CKEditor5 的 model 與 HTML 之間來回轉換時,有可能因為 conversion 沒寫好而多出<p>&nbsp;</p>。這雖然不會影響影片功能,但會讓 store 內容看起來很髒。後來我會在後端存檔前先做一次 HTML 清理,把連續空白段落合併。

第三個是「貼上網址時自動轉成影片」。很多人以為 CKEditor5 像 Word 一樣,貼上 YouTube 網址就會自動嵌入,其實預設不會。要達到這個效果,得去設定MediaEmbed的預設 provider,或者自己處理 paste 事件。如果你需要這個功能,可以在editor.model.document.on('clipboardInput')事件中攔截貼上內容,判斷是否為影片網址,再轉成指令執行。

6.3 下一步可以擴充的功能

如果你做完基本影片引入後還有餘力,我會建議往這幾個方向玩:

  • 拖拉上傳:把影片檔案拖進編輯器時自動上傳並插入。
  • 線上剪輯預覽:針對管理員上傳的影片,產生一張 poster 封面。
  • 浮動工具列:使用 BalloonEditor 或 DecoupledEditor,讓工具列可以在選取文字時彈出。
  • 多國語言按鈕:自訂 Plugin 的 label 可以依使用者語言切換。
  • 與檔案管理服務整合:例如接上 S3、OSS,甚至前面提到的 AList,讓上傳的檔案不只存在本機。

我自己最近的測試,是把這個 VideoPlugin 封成一個 Vue 3 component,讓不同後台頁面只要傳入toolbar設定就可以重用。跑通以後,整個團隊要接 CKEditor5 就變成「丟元件、設參數」的流程,維護成本低很多。

最後分享一個實際操作上的心得:如果你只是要在公司內部後台放影片,而且使用者都是固定一群人,不要一開始就想做到多完美。先照著這篇把按鈕、上傳、預覽串起來,讓大家實際用一週,再根據回饋調整工具列與格式限制。很多看似需要的功能,往往在真實使用後才發現其實用不到;反而是一些你沒想過的按鈕,像是「從原始碼貼 HTML」,才是管理員真正依賴的後門。工具列不要做太死,留一點彈性給進階使用者,之後你才不會一直被叫去改需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 9:53:48

工业级MRAM存储方案:STM32F745VG驱动MR25H40CDF实战

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

作者头像 李华
网站建设 2026/10/5 9:51:41

Android Slice锁屏日期首次正常后续不显示:从加载链路到根因定位

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

作者头像 李华
网站建设 2026/10/5 9:51:38

工业嵌入式存储选型:MRAM与dsPIC33FJ的SPI驱动实践

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

作者头像 李华
网站建设 2026/10/5 9:50:17

基于Three.js的三维视频融合技术实现与性能调优实战

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

作者头像 李华
网站建设 2026/10/5 9:47:22

SCAPS-1D光伏模拟从零到一:参数设置与缺陷建模实操指南

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

作者头像 李华