富文本編輯是管理後臺(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 瞭解更多