富文本编辑是管理后台(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 了解更多