富文本编辑是管理后台(cms)系统中的重要功能,编辑器的选择也非常多,如今大多编辑器都是走的简约路线,遇上挑剔的客户就无法满足他们的需求。百度的ueditor作为一款重量级的编辑器,提供了强大的功能,并且从word中直接copy到编辑器中的还原效果也非常好,但是由于官方已经很久没有维护了,所以对接已有的系统灵活度不够。
基于vue封装的ueditor组件挺多的,并且封装和改造的效果都还不错,比如vue-ueditor-wrap,在封装react-ueditor-component过程中也借鉴了开源社区中的优秀代码。
ueditor其他功能没什么需要改动的,但是上传文件的功能与后端耦合太高,不符合现在的前后端分离的系统设计,也不好对接第三方存储(如七牛OSS),所以要改造实现基本的两个功能:
另一块就是基础的编辑功能,封装后的组件应该像input
使用一样简单,value
控制编辑器内容,onChange
监听编辑器内容变化事件
下面解析一些核心功能的实现思路
ueditor
的初始化是异步的,所以需要在编辑器准备就绪后才能进行后续的操作,这里使用Promise
进行流程控制
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
就可以实现
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
并没有发生变化,如果不进行比较编辑器中的内容会被回退为旧值。
编辑器内容变化可以使用ueditor提供的contentChange,但是会有bug,比如按下多个按键时并不会触发该事件
react-ueditor-component
采用MutationObserver
监听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节流
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:
标记出来了,可以全局搜索查看所有源码改动
只需要找到获取配置的方法并修改就可以了,在_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 了解更多