富文本編輯是管理後臺(cms)系統中的重要功能,編輯器的選擇也很是多,現在大多編輯器都是走的簡約路線,趕上挑剔的客戶就沒法知足他們的需求。百度的ueditor做爲一款重量級的編輯器,提供了強大的功能,而且從word中直接copy到編輯器中的還原效果也很是好,可是因爲官方已經好久沒有維護了,因此對接已有的系統靈活度不夠。 基於vue封裝的ueditor組件挺多的,而且封裝和改造的效果都還不錯,好比vue-ueditor-wrap,在封裝react-ueditor-component過程當中也借鑑了開源社區中的優秀代碼。php
ueditor其餘功能沒什麼須要改動的,可是上傳文件的功能與後端耦合過高,不符合如今的先後端分離的系統設計,也很差對接第三方存儲(如七牛OSS),因此要改造實現基本的兩個功能:html
另外一塊就是基礎的編輯功能,封裝後的組件應該像input
使用同樣簡單,value
控制編輯器內容,onChange
監聽編輯器內容變化事件前端
下面解析一些核心功能的實現思路vue
ueditor
的初始化是異步的,因此須要在編輯器準備就緒後才能進行後續的操做,這裏使用Promise
進行流程控制node
componentDidMount () { // 編輯器ready後再進行後續操做 this.setState(state => ({ editorReady: new Promise((resolve, reject) => { let ueditor = window.UE.getEditor(this.editorId, { ...this.ueditorOptions, // 一些默認參數 ...this.props.ueditorOptions // props傳入的參數 }); ueditor.ready(() => { resolve(ueditor); this.observerChangeListener(ueditor); // 初始化監聽編輯器變化的方法,後面會具體說明 ueditor.setContent(this.props.value || ''); }); }) })); }
value改變觸發react-ueditor-component
中的編輯器的變化是個很簡單的父組件向子組件傳參, 使用static getDerivedStateFromProps
就能夠實現react
static getDerivedStateFromProps (nextProps, prevState) { let editorReady = prevState.editorReady; let value = nextProps.value; if (Object.prototype.hasOwnProperty.call(nextProps, 'value')) { editorReady && editorReady.then((ueditor) => { (value === prevState.content || value === ueditor.getContent()) || ueditor.setContent(value || ''); }); } return { ...prevState, content: value }; }
上面的代碼比想象中複雜一點,在組件內的state
中會建立一個屬性content
用於存儲上次傳過來的value
,props.value
會和content
和編輯器中實際的內容比較 由於在一些特殊狀況下,編輯器中的內容會發生變化,而同時getDerivedStateFromProps
會被觸發可是value
並無發生變化,若是不進行比較編輯器中的內容會被回退爲舊值。git
編輯器內容變化可使用ueditor提供的contentChange,可是會有bug,好比按下多個按鍵時並不會觸發該事件github
react-ueditor-component
採用MutationObserver
監聽DOM變化web
observerChangeListener (ueditor) { const changeHandle = () => { let onChange = this.props.onChange; if (ueditor.document.getElementById('baidu_pastebin')) { return; } onChange && onChange(ueditor.getContent()); }; this.observer = new MutationObserver(changeHandle); // FIXME: 這裏可使用debounce節流 this.observer.observe(ueditor.body, { attributes: true, // 是否監聽 DOM 元素的屬性變化 attributeFilter: ['src', 'style', 'type', 'name'], // 只有在該數組中的屬性值的變化纔會監聽 characterData: true, // 是否監聽文本節點 childList: true, // 是否監聽子節點 subtree: true // 是否監聽後代元素 }); }
此功能的實現須要修改ueditor
源碼了,筆者從fex-team/ueditorfork了一份,基於dev-1.4.3.3
分支建立了dev-3.0.0
分支,github,全部代碼的修改都用MARK:
標記出來了,能夠全局搜索查看全部源碼改動json
只須要找到獲取配置的方法並修改就能夠了,在_src/core
中
UE.Editor.prototype.loadServerConfig = function(){ this._serverConfigLoaded = false; try { utils.extend(this.options, this.options.serverOptions); utils.extend(this.options, this.options.serverExtra); this.fireEvent('serverConfigLoaded'); this._serverConfigLoaded = true; } catch (e) { console.error(this.getLang('loadconfigFormatError')); } }
相應的,封裝的react-ueditor-component
增長了字段配置
window.UE.getEditor(this.editorId, { serverUrl: this.props.ueditorOptions.serverUrl, serverOptions: { imageActionName: 'uploadimage', imageFieldName: 'file', ...others }, serverExtra: this.props.ueditorOptions.serverUrl });
beforeUpload
鉤子是自定義請求數據實現的關鍵,但實現的功能又不止於增長自定義請求數據
beforeUpload
方法由參數傳入ueditor
上傳前須要進行的操做不少狀況下多是一個異步過程,這裏使用Promise
進行流程控制,以autoupload.js
爲例
if (me.options.beforeUpload) { Promise.resolve(me.options.beforeUpload(file)).then(function (file) { if (!file) { return } // 設置請求頭和請求內容,開始上傳 }) } else { // 設置請求頭和請求內容,開始上傳 }
自定義請求數據用serverExtra
實現,須要這部份內容是隨時可變的,因此須要新增一個方法,能夠隨時設置serverExtra
UE.Editor.prototype.setExtraData = function (options) { try { utils.extend(this.options, options); } catch (e) { console.error(this.getLang('setExtraconfigFormatError')); } }
上面的代碼不難看出來,實際上setExtraData
方法能夠設置任何配置,可是後續封裝組件並使用時,我只建議用於修改serverExtra
,由於修改ueditor的其餘參數並不必定有效,而且可能會出現沒法預期的bug。
在每次執行上傳以前應該讀取配置、設置上傳內容,以autoupload.js
爲例
var fd = new FormData() // 請求體中增長額外數據 if (me.options.extraData && Object.prototype.toString.apply(me.options.extraData) === "[object Object]") { for (var key in me.options.extraData) { fd.append(key, me.options.extraData[key]); } } // 請求頭中增長額外數據 if (me.options.headers && Object.prototype.toString.apply(me.options.headers) === "[object Object]") { for (var key in me.options.headers) { xhr.setRequestHeader(key, me.options.headers[key]); } }
封裝在組件中,須要在static getDerivedStateFromProps
中實現響應式更新
if (Object.prototype.hasOwnProperty.call(nextProps.ueditorOptions, 'serverExtra')) { let serverExtraStr = JSON.stringify(nextProps.ueditorOptions.serverExtra); if (serverExtraStr === prevState.serverExtraStr) { return { ...prevState, content: value }; } editorReady && editorReady.then((ueditor) => { ueditor.setExtraData && ueditor.setExtraData(nextProps.ueditorOptions.serverExtra); }); return { ...prevState, serverExtraStr, content: value }; }
以上即是ueditor改造和封裝中最核心的內容,下面簡單介紹一下應該如何使用react-ueditor-component
,詳細的使用教程請看readme.md,項目源碼中也提供了完整的demo
,App.js
(不使用react-ueditor-component
)、OwnServer.js
(使用react-ueditor-component
上傳到本身的服務器)、QiniuServer.js
(使用react-ueditor-component
對接七牛OSS)。
安裝組件
yarn add react-ueditor-component --save
下載修改後打包的ueditor.zip,或者找到node_modules/react-ueditor-component/assets/utf8-php.zip
,解壓文件,放在網站的根目錄,react項目通常放在public
文件夾下, index.html
中script
標籤引入ueditor
代碼
<script src="/utf8-php/ueditor.config.js"></script> <script src="/utf8-php/ueditor.all.js"></script>
import ReactUEditorComponent from 'react-ueditor-component'; export default class App extends React.Component { state = { value: '' } onChange = (value) => this.setState(value); render () { <div> <ReactUEditorComponent value={this.state.value} onChange={this.onChange} /> {/* 配合antd的form */} { this.props.form.getFieldDecorator('content')( <ReactUEditorComponent /> ) } </div> } }
beforeUpload
鉤子一般對接第三方OSS須要獲取上傳憑證,這就須要用到beforeUpload
鉤子
export default class App extends React.Component { state = { value: '', serverExtra: { // 上傳文件額外的數據 extraData: {} } } beforeUpload = file => new Promise((resolve, reject) => { let key = 't' + Math.random().toString().slice(5, 16); // 請求服務器,獲取七牛上傳憑證 fetch('getuploadtoken.com', { headers }) .then(response => response.json()) .then((data) => { // 設置七牛直傳額外數據 this.setState({ serverExtra: { extraData: { token: data.token, key } }, // 設置額外數據完成會觸發`setExtraDataComplete` setExtraDataComplete: () => { resolve(file); } }); }); }) onChange = (value) => this.setState(value); render () { return ( <ReactUEditorComponent value={this.state.value} onChange={this.onChange} // 必須在state中 setExtraDataComplete={this.state.setExtraDataComplete} ueditorOptions={{ beforeUpload: this.beforeUpload, // 上傳文件時的額外信息 serverExtra: this.state.serverExtra, serverUrl: 'http://qiniuupload.com' // 上傳文件的接口 }} /> ) } }
但願以上輪子有朝一日對你有所幫助,歡迎提供技術支持,或者加入咱們 yemao@talkmoney.cn
做者簡介:葉茂,蘆葦科技web前端開發工程師,表明做品:口紅挑戰網紅小遊戲、服務端渲染官網、微信小程序粒子系統。擅長網站建設、公衆號開發、微信小程序開發、小遊戲、公衆號開發,專一於前端領域框架、交互設計、圖像繪製、數據分析等研究。 一塊兒並肩做戰: yemao@talkmoney.cn 訪問 www.talkmoney.cn 瞭解更多