本文永久連結:github.com/HaoChuan942…
前言
封面是UEditor
的 百度指數 折線圖。雖然今天已經是 2018 年,且優秀的富文字編輯器層出不窮(包括移動端),但從圖中可以看出UEditor
仍然維持著較高的搜尋熱度。而不少公司和個人也仍然在專案中使用UEditor
。目前,UEditor
官網的最後一次版本更新是 1.4.3.3,這已經是 2016 年的事情了,而今天的前端開發,很多小夥伴都在使用Vue
,React
這種元件化的前端框架。這就導致在這些“現代”框架中整合UEditor
變得很不平滑。所以才會有下圖這些大量介紹如何在Vue
專案中整合UEditor
的部落格:
為了提高程式碼的可複用性,也為了儘可能的不在業務程式碼中參雜UEditor
的相關操作,我在幾個月前,公司專案的開發中擼了一個元件,可以通過v-model
雙向繫結的方式來使用UEditor
,簡單到就像使用input
框一樣。當我擼完,感覺非常的Vue
範兒。而且看了不少部落格和GitHub
專案,都沒有類似的實現。於是我決定釋出到 npm 上,幫助一眾還在思考如何把UEditor
整合到Vue
專案中的小夥伴。幾個月下來,基本已經穩定,所以,今天通過這篇部落格,分享給大家。
先看效果圖:
Installation
npm i vue-ueditor-wrap
# 或者
yarn add vue-ueditor-wrap
複製程式碼
Quick Start
-
下載 UEditor下載最新編譯的 UEditor。官網目前最新的版本是
1.4.3.3
,存在諸多 BUG,例如 Issue1,且官方不再積極維護。為了世界的和平,針對一些常見 BUG,我進行了修復,並把編譯好的檔案放在了本倉庫的assets/downloads
目錄下,你可以放心下載,當然你也可以自己clone
官方原始碼並編譯。將下載的壓縮包解壓並重新命名為
UEditor
(只需要選擇一個你需要的版本,比如utf8-php
),放入你專案的static
目錄下。如果你使用的是 vue-cli 3.x,可以把
UEditor
資料夾放入專案的public
目錄下。 -
引入
VueUeditorWrap
元件import VueUeditorWrap from 'vue-ueditor-wrap' // ES6 Module // 或者 const VueUeditorWrap = require('vue-ueditor-wrap') // CommonJS 複製程式碼
你也可以通過直接引入
CDN
連結的方式來使用,它會暴露一個全域性的VueUeditorWrap
變數(具體如何使用你可以閱讀我的這篇部落格或參考這個倉庫)。<script src="https://cdn.jsdelivr.net/npm/vue-ueditor-wrap@latest/lib/vue-ueditor-wrap.min.js"></script> 複製程式碼
-
註冊元件
components: { VueUeditorWrap } // 或者在 main.js 裡將它註冊為全域性元件 Vue.component('vue-ueditor-wrap', VueUeditorWrap) 複製程式碼
-
v-model
繫結資料<vue-ueditor-wrap v-model="msg"></vue-ueditor-wrap> 複製程式碼
data () { return { msg: '<h2>Vue + UEditor + v-model雙向繫結</h2>' } } 複製程式碼
至此你已經可以在頁面中看到一個初始化之後的
UEditor
了,並且它已經成功和資料繫結了!??? -
根據專案需求修改配置,完整配置選項檢視 ueditor.config.js 原始碼或 官方文件
<vue-ueditor-wrap v-model="msg" :config="myConfig"></vue-ueditor-wrap> 複製程式碼
data () { return { msg: '<h2>Vue + UEditor + v-model雙向繫結</h2>', myConfig: { // 編輯器不自動被內容撐高 autoHeightEnabled: false, // 初始容器高度 initialFrameHeight: 240, // 初始容器寬度 initialFrameWidth: '100%', // 上傳檔案介面(這個地址是我為了方便各位體驗檔案上傳功能搭建的臨時介面,請勿在生產環境使用!!!) serverUrl: 'http://35.201.165.105:8000/controller.php', // UEditor 資原始檔的存放路徑,如果你使用的是 vue-cli 生成的專案,通常不需要設定該選項,vue-ueditor-wrap 會自動處理常見的情況,如果需要特殊配置,參考下方的常見問題2 UEDITOR_HOME_URL: '/static/UEditor/' } } } 複製程式碼
Advanced
-
如何獲取
UEditor
例項?<vue-ueditor-wrap @ready="ready"></vue-ueditor-wrap> 複製程式碼
methods: { ready (editorInstance) { console.log(`編輯器例項${editorInstance.key}: `, editorInstance) } } 複製程式碼
-
設定是否在元件的
beforeDestroy
鉤子裡銷燬UEditor
例項<vue-ueditor-wrap :destroy="true"></vue-ueditor-wrap> 複製程式碼
-
選取
v-model
的實現方式。雙向繫結的實現依賴對編輯器內容變化的監聽,由於監聽方式的不同,會帶來監聽效果的差異性,你可以自行選擇,但建議使用開箱即用的預設值。<vue-ueditor-wrap mode="listener"></vue-ueditor-wrap> 複製程式碼
可選值:
observer
,listener
預設值:
observer
引數說明:
-
observer
模式藉助 MutationObserver API。優點在於監聽的準確性,缺點在於它會帶來一點額外的效能開銷。你可以通過observerDebounceTime
屬性設定觸發間隔,還可以通過observerOptions
屬性有選擇的設定 MutationObserver 的監聽行為。該 API 只相容到 IE11+,但vue-ueditor-wrap
會在不支援的瀏覽器中自動啟用listener
模式。<vue-ueditor-wrap mode="observer" :observerDebounceTime="100" :observerOptions="{ attributes: true, characterData: true, childList: true, subtree: true }" > </vue-ueditor-wrap> 複製程式碼
-
listener
模式藉助 UEditor 的 contentChange 事件,優點在於依賴官方提供的事件 API,無需額外的效能消耗,相容性更好,但缺點在於監聽的準確性並不高,存在如下方 [常見問題 5] 中的提到的 BUG。
-
-
是否支援
Vue SSR
?自
2.4.0
版本開始支援服務端渲染!本元件提供對Nuxt
專案開箱即用的支援。但如果你是自己搭建的Vue SSR
專案,你可能需要自行區分服務端和客戶端環境並結合forceInit
屬性強制初始化編輯器,但大概率你用不到該屬性,即使是自己搭建的 SSR 專案,更多問題歡迎提交 ISSUE。 -
如何進行二次開發(新增自定義按鈕、彈窗等)?
本元件提供了
beforeInit
鉤子,它會在UEditor
的 scripts 載入完畢之後、編輯器初始化之前觸發,你可以在此時機,通過操作 window.UE 物件,來進行諸如新增自定義按鈕、彈窗等的二次開發。beforeInit
的觸發函式以 編輯器 id 和 配置引數 作為入參。下面提供了一個簡單的自定義按鈕和自定義彈窗的示例,DEMO 倉庫中也提供了自定義“表格居中”按鈕的示例,如果有更多二次開發的需求,你可以參考官方 API 或者 UEditor 原始碼 中的示例。自定義按鈕 Demo
<vue-ueditor-wrap v-model="msg" @beforeInit="addCustomButtom"></vue-ueditor-wrap> 複製程式碼
addCustomButtom (editorId) { window.UE.registerUI('test-button', function (editor, uiName) { // 註冊按鈕執行時的 command 命令,使用命令預設就會帶有回退操作 editor.registerCommand(uiName, { execCommand: function () { editor.execCommand('inserthtml', `<span>這是一段由自定義按鈕新增的文字</span>`) } }) // 建立一個 button var btn = new window.UE.ui.Button({ // 按鈕的名字 name: uiName, // 提示 title: '滑鼠懸停時的提示文字', // 需要新增的額外樣式,可指定 icon 圖示,圖示路徑參考常見問題 2 cssRules: "background-image: url('/test-button.png') !important;background-size: cover;", // 點選時執行的命令 onclick: function () { // 這裡可以不用執行命令,做你自己的操作也可 editor.execCommand(uiName) } }) // 當點到編輯內容上時,按鈕要做的狀態反射 editor.addListener('selectionchange', function () { var state = editor.queryCommandState(uiName) if (state === -1) { btn.setDisabled(true) btn.setChecked(false) } else { btn.setDisabled(false) btn.setChecked(state) } }) // 因為你是新增 button,所以需要返回這個 button return btn }, 0 /* 指定新增到工具欄上的哪個位置,預設時追加到最後 */, editorId /* 指定這個 UI 是哪個編輯器例項上的,預設是頁面上所有的編輯器都會新增這個按鈕 */) } 複製程式碼
自定義彈窗 Demo
<vue-ueditor-wrap v-model="msg" @beforeInit="addCustomDialog"></vue-ueditor-wrap> 複製程式碼
addCustomDialog (editorId) { window.UE.registerUI('test-dialog', function (editor, uiName) { // 建立 dialog var dialog = new window.UE.ui.Dialog({ // 指定彈出層中頁面的路徑,這裡只能支援頁面,路徑參考常見問題 2 iframeUrl: '/customizeDialogPage.html', // 需要指定當前的編輯器例項 editor: editor, // 指定 dialog 的名字 name: uiName, // dialog 的標題 title: '這是一個自定義的 Dialog 浮層', // 指定 dialog 的外圍樣式 cssRules: 'width:600px;height:300px;', // 如果給出了 buttons 就代表 dialog 有確定和取消 buttons: [ { className: 'edui-okbutton', label: '確定', onclick: function () { dialog.close(true) } }, { className: 'edui-cancelbutton', label: '取消', onclick: function () { dialog.close(false) } } ] }) // 參考上面的自定義按鈕 var btn = new window.UE.ui.Button({ name: 'dialog-button', title: '滑鼠懸停時的提示文字', cssRules: `background-image: url('/test-dialog.png') !important;background-size: cover;`, onclick: function () { // 渲染dialog dialog.render() dialog.open() } }) return btn }, 0 /* 指定新增到工具欄上的那個位置,預設時追加到最後 */, editorId /* 指定這個UI是哪個編輯器例項上的,預設是頁面上所有的編輯器都會新增這個按鈕 */) } 複製程式碼
彈出層中的 HTML 頁面
customizeDialogPage.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Title</title> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="renderer" content="webkit"> <!--頁面中一定要引入internal.js為了能直接使用當前開啟dialog的例項變數--> <!--internal.js預設是放到 UEditor/dialogs 目錄下的--> <script type="text/javascript" src="./UEditor/dialogs/internal.js"></script> </head> <body> <h1>hello vue-ueditor-wrap</h1> <script> //可以直接使用以下全域性變數 //當前開啟dialog的例項變數 console.log('editor: ' + editor); //一些常用工具 console.log('domUtils: ' + domUtils); console.log('utils: ' + utils); console.log('browser: ' + browser); dialog.onok = function() { editor.execCommand('inserthtml', '<span>我點選了確定</span>'); }; dialog.oncancel = function() { editor.execCommand('inserthtml', '<span>我點選了取消</span>'); }; </script> </body> </html> 複製程式碼
Features
-
v-model
雙向資料繫結!你不需要考慮例項化,也不需要考慮何時getContent
,何時setContent
,簡單到像使用input
框一樣! -
完全遵從官方
API
,所有的配置引數和例項方法與官方完全一致。通過給vue-ueditor-wrap
元件的config
屬性傳遞一個物件,你就可以得到一個完全獨立配置的UEditor
編輯器。通過監聽ready
事件你就可以得到初始化後的UEditor
例項並執行例項上的各種方法。 -
自動新增依賴檔案。你不需要自己在
index.html
或main.js
裡引入UEditor
的 JS 檔案。更重要的是即使你在一個頁面裡同時使用多個vue-ueditor-wrap
元件,它所依賴的 JS 檔案也只會載入一次。這麼做的原因在於你不需要當使用者一開啟專案就先載入大量UEditor
相關的資源,所有的資原始檔只會在vue-ueditor-wrap
元件第一次被啟用時才載入。當然,如果你在index.html
或main.js
裡引入了相關資源,vue-ueditor-wrap
也會準確判斷,你不用擔心它會重複載入。 -
每個
vue-ueditor-wrap
元件是完全獨立的。你甚至可以在上面使用v-for
指令一次渲染 99個 兔斯基(不要忘記新增key
值)。
FAQ(常見問題)
-
是否支援
IE
等低版本瀏覽器?與
Vue
相同,整體支援到IE9+
??? -
為什麼我會看到這個報錯?
這是
UEDITOR_HOME_URL
引數配置錯誤導致的。在 vue cli 2.x 生成的專案中使用本元件,預設值是'/static/UEditor/'
,在 vue cli 3.x 生成的專案中,預設值是process.env.BASE_URL + 'UEditor/'
。但這並不能滿足所有情況。例如你的專案不是部署在網站根目錄下,如"http://www.example.com/my-app/"
,你可能需要設定為"/my-app/static/UEditor/"
。是否使用了相對路徑、路由是否使用history
模式、伺服器配置是否正確等等都有可能會產生影響。總而言之:無論本地開發和部署到伺服器,你所指定的UEditor
資原始檔是需要真實存在的,vue-ueditor-wrap
也會在 JS 載入失敗時通過 console 輸出它試圖去載入的資原始檔的完整路徑,你可以藉此分析如何填寫。當需要區分環境時,你可以通過判斷process.env.NODE_ENV
來分別設定。 -
我該如何上傳圖片和檔案?為什麼我會看到
後臺配置項返回格式出錯
?上傳圖片、檔案等功能是需要與後臺配合的,而你沒有給
config
屬性傳遞正確的serverUrl
,我提供了http://35.201.165.105:8000/controller.php
的臨時介面,你可以用於測試,但切忌在生產環境使用!!! 關於如何搭建上傳介面,可以參考官方文件。 -
單圖片跨域上傳失敗!
UEditor
的單圖上傳是通過 Form 表單 + iframe 的方式實現的,但由於同源策略的限制,父頁面無法訪問跨域 iframe 的文件內容,所以會出現單圖片跨域上傳失敗的問題。我通過 XHR 重構了單圖上傳的方式,下載最新編譯的 UEditor 資原始檔即可在IE10+
的瀏覽器中實現單圖跨域上傳了。具體細節,點此檢視。當然你也可以通過配置toolbars
引數來隱藏單圖片上傳按鈕,並結合上面介紹的“自定義按鈕”,曲線救國,以下程式碼僅供參考。var input = document.createElement('input') input.type = "file" input.style.display = 'none' document.body.appendChild(input) input.click() input.addEventListener('change',(e)=>{ // 利用 AJAX 上傳,上傳成功之後銷燬 DOM console.log(e.target.files) }) 複製程式碼
-
為什麼我輸入的
"? ! $ #"
這些特殊字元,沒有成功繫結?當你使用
listener
模式時,由於v-model
的實現是基於對UEditor
例項上contentChange
事件的監聽,而你輸入這些特殊字元時通常是按住shift
鍵的,UEditor
本身的contentChange
在shift
鍵按住時不會觸發,你也可以嘗試同時按下多個鍵,你會發現contentChange
只觸發一次。你可以使用observer
模式或移步 UEditor。 -
單圖片上傳後
v-model
繫結的是loading
小圖示。這個也是
UEditor
的BUG
。我最新編輯的版本,修復了官方的這個BUG
,如果你使用的是官網下載的資原始檔,請替換資原始檔或參考 Issue1。
更多問題,歡迎提交 ISSUE 或者去 聊天室 提問。但由於這是一個個人維護的專案,我平時也有自己的工作,所以並不能保證及時解決你們的所有問題,如果小夥伴們有好的建議或更炫酷的操作,也歡迎
PR
,如果你覺得這個元件給你的開發帶來了實實在在的方便,也非常感謝你的Star
,當然還有咖啡:
程式碼修改請遵循指定的
ESLint
規則,PR
之前請先執行npm run lint
進行程式碼風格檢測,大部分語法細節可以通過npm run fix
修正,構建之後,記得修改package.json
裡的版本號,方便我Review
通過後麻溜溜的釋出到npm
。
總結
雖然這是一次很小的創新,UEditor
也可能是一個過氣的富文字編輯器。但是在維護這個專案以及幫助一眾小夥伴解決ISSUE
的過程中,我成長了很多。最令我感動的是不少小夥伴還給我郵箱發了感謝信,而且我還發現確實已經有一些人開始在專案中用了。這種被他人認可,以及幫助別人的快樂真的只有體會過的人才知道。也就在前不久,我決定開始在掘金寫部落格,雖然一些東西寫的不那麼好,或者自己認知有錯誤,但總有一群熱心且優秀的小夥伴,會在評論區指正以及給出寶貴的意見。分享是快樂的!所以,我的這篇文章也權當拋磚引玉,如果小夥伴們有好的建議或更炫酷的操作,也歡迎PR
,不過PR
之前請先執行npm run lint
進行程式碼風格檢測,大部分語法細節也可以通過npm run fix
修正,也要記得修改package.json
的版本號version
,方便我直接釋出到npm
。當然如果你有好用的富文字編輯器,也可以在評論區推薦。