背景
富文本編輯是管理后臺(cms)系統(tǒng)中的重要功能唆鸡,編輯器的選擇也非常多涝影,如今大多編輯器都是走的簡約路線,遇上挑剔的客戶就無法滿足他們的需求喇闸。百度的ueditor作為一款重量級的編輯器袄琳,提供了強大的功能,并且從word中直接copy到編輯器中的還原效果也非常好燃乍,但是由于官方已經(jīng)很久沒有維護了宛琅,所以對接已有的系統(tǒng)靈活度不夠。
基于vue封裝的ueditor組件挺多的嘿辟,并且封裝和改造的效果都還不錯,比如vue-ueditor-wrap红伦,在封裝react-ueditor-component過程中也借鑒了開源社區(qū)中的優(yōu)秀代碼。
功能需求
ueditor其他功能沒什么需要改動的昙读,但是上傳文件的功能與后端耦合太高,不符合現(xiàn)在的前后端分離的系統(tǒng)設計,也不好對接第三方存儲(如七牛OSS)只嚣,所以要改造實現(xiàn)基本的兩個功能:
- 后端配置前移,上傳文件的配置參數(shù)直接寫在前端册舞,不需要請求一次后端接口才能初始化上傳文件的功能
- 自定義上傳文件的請求頭和請求,雖然官方提供了解決方案调鲸,但是不夠靈活
另一塊就是基礎的編輯功能,封裝后的組件應該像input
使用一樣簡單线得,value
控制編輯器內(nèi)容,onChange
監(jiān)聽編輯器內(nèi)容變化事件
下面解析一些核心功能的實現(xiàn)思路
初始化編輯器
ueditor
的初始化是異步的贯钩,所以需要在編輯器準備就緒后才能進行后續(xù)的操作,這里使用Promise
進行流程控制
componentDidMount () {
// 編輯器ready后再進行后續(xù)操作
this.setState(state => ({
editorReady: new Promise((resolve, reject) => {
let ueditor = window.UE.getEditor(this.editorId, {
...this.ueditorOptions, // 一些默認參數(shù)
...this.props.ueditorOptions // props傳入的參數(shù)
});
ueditor.ready(() => {
resolve(ueditor);
this.observerChangeListener(ueditor); // 初始化監(jiān)聽編輯器變化的方法办素,后面會具體說明
ueditor.setContent(this.props.value || '');
});
})
}));
}
value
value改變觸發(fā)react-ueditor-component
中的編輯器的變化是個很簡單的父組件向子組件傳參角雷,
使用static getDerivedStateFromProps
就可以實現(xiàn)
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
};
}
上面的代碼比想象中復雜一點,在組件內(nèi)的state
中會創(chuàng)建一個屬性content
用于存儲上次傳過來的value
性穿,props.value
會和content
和編輯器中實際的內(nèi)容比較
因為在一些特殊情況下勺三,編輯器中的內(nèi)容會發(fā)生變化,而同時getDerivedStateFromProps
會被觸發(fā)但是value
并沒有發(fā)生變化需曾,如果不進行比較編輯器中的內(nèi)容會被回退為舊值吗坚。
onChange
編輯器內(nèi)容變化可以使用ueditor提供的contentChange,但是會有bug呆万,比如按下多個按鍵時并不會觸發(fā)該事件
react-ueditor-component
采用MutationObserver
監(jiān)聽DOM變化
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節(jié)流
this.observer.observe(ueditor.body, {
attributes: true, // 是否監(jiān)聽 DOM 元素的屬性變化
attributeFilter: ['src', 'style', 'type', 'name'], // 只有在該數(shù)組中的屬性值的變化才會監(jiān)聽
characterData: true, // 是否監(jiān)聽文本節(jié)點
childList: true, // 是否監(jiān)聽子節(jié)點
subtree: true // 是否監(jiān)聽后代元素
});
}
后端配置前移
此功能的實現(xiàn)需要修改ueditor
源碼了商源,筆者從fex-team/ueditorfork了一份,基于dev-1.4.3.3
分支創(chuàng)建了dev-3.0.0
分支谋减,github牡彻,所有代碼的修改都用MARK:
標記出來了,可以全局搜索查看所有源碼改動
只需要找到獲取配置的方法并修改就可以了出爹,在_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
鉤子是自定義請求數(shù)據(jù)實現(xiàn)的關鍵,但實現(xiàn)的功能又不止于增加自定義請求數(shù)據(jù)
beforeUpload
方法由參數(shù)傳入ueditor
上傳前需要進行的操作很多情況下可能是一個異步過程严就,這里使用Promise
進行流程控制总寻,以autoupload.js
為例
if (me.options.beforeUpload) {
Promise.resolve(me.options.beforeUpload(file)).then(function (file) {
if (!file) {
return
}
// 設置請求頭和請求內(nèi)容,開始上傳
})
} else {
// 設置請求頭和請求內(nèi)容梢为,開始上傳
}
自定義請求數(shù)據(jù)
自定義請求數(shù)據(jù)用serverExtra
實現(xiàn)渐行,需要這部分內(nèi)容是隨時可變的轰坊,所以需要新增一個方法,可以隨時設置serverExtra
UE.Editor.prototype.setExtraData = function (options) {
try {
utils.extend(this.options, options);
} catch (e) {
console.error(this.getLang('setExtraconfigFormatError'));
}
}
上面的代碼不難看出來殊轴,實際上setExtraData
方法可以設置任何配置衰倦,但是后續(xù)封裝組件并使用時,我只建議用于修改serverExtra
旁理,因為修改ueditor的其他參數(shù)并不一定有效樊零,并且可能會出現(xiàn)無法預期的bug。
在每次執(zhí)行上傳之前應該讀取配置孽文、設置上傳內(nèi)容驻襟,以autoupload.js
為例
var fd = new FormData()
// 請求體中增加額外數(shù)據(jù)
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]);
}
}
// 請求頭中增加額外數(shù)據(jù)
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
中實現(xiàn)響應式更新
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改造和封裝中最核心的內(nèi)容芋哭,下面簡單介紹一下應該如何使用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
吸占,解壓文件矾屯,放在網(wǎng)站的根目錄菌湃,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: {
// 上傳文件額外的數(shù)據(jù)
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) => {
// 設置七牛直傳額外數(shù)據(jù)
this.setState({
serverExtra: {
extraData: {
token: data.token,
key
}
},
// 設置額外數(shù)據(jù)完成會觸發(fā)`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前端開發(fā)工程師,代表作品:口紅挑戰(zhàn)網(wǎng)紅小游戲西篓、服務端渲染官網(wǎng)憋活、微信小程序粒子系統(tǒng)。擅長網(wǎng)站建設吮成、公眾號開發(fā)粱甫、微信小程序開發(fā)冗美、小游戲、公眾號開發(fā)节预,專注于前端領域框架属韧、交互設計、圖像繪制糠赦、數(shù)據(jù)分析等研究拙泽。 一起并肩作戰(zhàn): yemao@talkmoney.cn 訪問 www.talkmoney.cn 了解更多