本文永久連接:github.com/HaoChuan942…javascript
封面是UEditor
的 百度指數 折線圖。雖然今天已是 2018 年,且優秀的富文本編輯器層出不窮(包括移動端),但從圖中能夠看出UEditor
仍然維持着較高的搜索熱度。而很多公司和我的也仍然在項目中使用UEditor
。目前,UEditor
官網的最後一次版本更新是 1.4.3.3,這已是 2016 年的事情了,而今天的前端開發,不少小夥伴都在使用Vue
,React
這種組件化的前端框架。這就致使在這些「現代」框架中集成UEditor
變得很不平滑。因此纔會有下圖這些大量介紹如何在Vue
項目中集成UEditor
的博客:php
爲了提升代碼的可複用性,也爲了儘量的不在業務代碼中參雜UEditor
的相關操做,我在幾個月前,公司項目的開發中擼了一個組件,能夠經過v-model
雙向綁定的方式來使用UEditor
,簡單到就像使用input
框同樣。當我擼完,感受很是的Vue
範兒。並且看了很多博客和GitHub
項目,都沒有相似的實現。因而我決定發佈到 npm 上,幫助一衆還在思考如何把UEditor
集成到Vue
項目中的小夥伴。幾個月下來,基本已經穩定,因此,今天經過這篇博客,分享給你們。css
先看效果圖:html
npm i vue-ueditor-wrap
# 或者
yarn add vue-ueditor-wrap
複製代碼
下載 UEditorjava
下載最新編譯的 UEditor。官網目前最新的版本是
1.4.3.3
,存在諸多 BUG,例如 Issue1,且官方再也不積極維護。爲了世界的和平,針對一些常見 BUG,我進行了修復,並把編譯好的文件放在了本倉庫的assets/downloads
目錄下,你能夠放心下載,固然你也能夠本身clone
官方源碼並編譯。git
將下載的壓縮包解壓並重命名爲 UEditor
(只須要選擇一個你須要的版本,好比 utf8-php
),放入你項目的 static
目錄下。github
若是你使用的是 vue-cli 3.x,能夠把
UEditor
文件夾放入項目的public
目錄下。web
引入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/'
}
}
}
複製代碼
如何獲取 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 源碼 中的示例。
<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 是哪一個編輯器實例上的,默認是頁面上全部的編輯器都會添加這個按鈕 */)
}
複製代碼
<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>
複製代碼
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
值)。
是否支持 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
。固然若是你有好用的富文本編輯器,也能夠在評論區推薦。