React项目中使用wangeditor以及扩展上传附件菜单
在最近的工作中需要用到富文本编辑器,结合项目的UI样式以及业务需求,选择了wangEditor。另外在使用的过程中发现wangEditor只有上传图片和视频的功能,没有上传文本附件的功能,所以需要对其扩展一个上传附件的功能。
我们的项目前端是用的react框架,在这里就记录一下我在项目中对wangEditor的简单封装使用以及扩展上传附件菜单。
需要购买阿里云产品和服务的,点击此链接领取优惠券红包,优惠购买哦,领取后一个月内有效: https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=fp9ccf07
1、npm 或yarn安装 wangEditor
yarn add wangeditor -S

2、封装成一个简单的组件
在components/common目录下新建一个editor文件夹,该文件夹下是封装的组件,
目录结构如下:

下面直接贴代码
2.1、index.jsx:
import React, { Component } from 'react';
import { message, Spin } from 'antd';
import Wangeditor from 'wangeditor';
import fileMenu from './fileMenu';
import $axios from '@/request';
/**
* 对wangEditor进行封装后的富文本编辑器组件,引用该组件时可传入一下参数
* isUploadFile: 是否可上传附件(自定义扩展菜单)
* defaultHtml: 默认初始化内容
* height: 设置编辑器高度
* uploadFileServer:附件上传接口地址
* maxFileSize:上传附件大小最大限制(单位:M)
* uploadImgServer:图片上传接口地址
* maxImgSize:上传图片大小最大限制(单位:M)
* menus: 可显示的菜单项
*/
export default class Editor extends Component {
constructor(props) {
super(props)
this.containerRef = React.createRef();
this.state = {
isUploading: false, //是否正在上传附件或图片
}
}
componentDidMount = () => {
const div = this.containerRef.current;
const editor = new Wangeditor(div);
editor.config.height = this.props?.height || 200;
editor.config.menus = this.props?.menus || [
'head', // 标题
'bold', // 粗体
'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'lineHeight', // 行高
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'quote', // 引用
'emoticon', // 表情
'image', // 插入图片
'table', // 表格
// 'video', // 插入视频
// 'code', // 插入代码
// 'undo', // 撤销
// 'redo' // 重复
];
this.editor = editor;
this.setCustomConfig();
editor.create();
editor.txt.html(this?.props?.defaultHtml)
// 要放在editor实例化之后创建上传菜单
this?.props?.isUploadFile &&
fileMenu(
editor,
this.containerRef.current,
{
uploadFileServer: this.props?.uploadFileServer, // 附件上传接口地址
maxFileSize: this.props?.maxFileSize || 10, // 限制附件最大尺寸(单位:M)
},
this.changeUploading
);
};
changeUploading = (flag) => {
this.setState({ isUploading: flag });
}
onChange = html => {
this?.props?.onChange(html);
};
// 上传图片
setCustomConfig = () => {
const _this = this;
const { customConfig } = this.props
this.editor.customConfig = {
// 关闭粘贴内容中的样式
pasteFilterStyle: false,
// 忽略粘贴内容中的图片
pasteIgnoreImg: true,
...customConfig,
}
const uploadImgServer = this.props?.uploadImgServer; // 上传图片的地址
const maxLength = 1; // 限制每次最多上传图片的个数
const maxImgSize = 2; // 上传图片的最大大小(单位:M)
const timeout = 1 * 60 * 1000 // 超时 1min
let resultFiles = [];
// this.editor.config.uploadImgMaxSize = maxImgSize * 1024 * 1024; // 上传图片大小2M
this.editor.config.uploadImgMaxLength = maxLength; // 限制一次最多上传 1 张图片
this.editor.config.customUploadImg = function (files, insert) { //上传图片demo
_this.changeUploading(true);
for (let file of files) {
const name = file.name
const size = file.size
// chrome 低版本 name === undefined
if (!name || !size) {
_this.changeUploading(false);
return;
}
if (maxImgSize * 1024 * 1024 < size) {
// 上传附件过大
message.warning('上传附件不可超过' + maxImgSize + 'M');
_this.changeUploading(false);
return;
}
// 验证通过的加入结果列表
resultFiles.push(file);
}
console.log(resultFiles)
if (resultFiles.length > maxLength) {
message.warning('一次最多上传' + maxLength + '个文件');
_this.changeUploading(false);
return;
}
// files 是 input 中选中的文件列表
const formData = new window.FormData();
formData.append('file', files[0]);
if (uploadImgServer && typeof uploadImgServer === 'string') {
// 定义 xhr
const xhr = new XMLHttpRequest()
xhr.open('POST', uploadImgServer)
// 设置超时
xhr.timeout = timeout
xhr.ontimeout = function () {
message.error('上传图片超时')
}
// 监控 progress
if (xhr.upload) {
xhr.upload.onprogress = function (e) {
let percent = void 0
// 进度条
if (e.lengthComputable) {
percent = e.loaded / e.total
console.log('上传进度:', percent);
}
}
}
// 返回数据
xhr.onreadystatechange = function () {
let result = void 0
if (xhr.readyState === 4) {
if (xhr.status < 200 || xhr.status >= 300) {
message.error('上传失败');
_this.changeUploading(false);
resultFiles = [];
return;
}
result = xhr.responseText
if ((typeof result === 'undefined' ? 'undefined' : typeof result) !== 'object') {
try {
result = JSON.parse(result)
} catch (ex) {
message.error('上传失败');
_this.changeUploading(false);
resultFiles = [];
return;
}
}
const res = result || []
if (res?.code == 200) {
// 上传代码返回结果之后,将图片插入到编辑器中
insert(res?.data?.url || '');
_this.changeUploading(false);
resultFiles = [];
}
}
}
// 自定义 headers
xhr.setRequestHeader('token', sessionStorage.getItem('token'));
// 跨域传 cookie
xhr.withCredentials = false
// 发送请求
xhr.send(formData);
}
};
};
render() {
return (
<Spin spinning={this.state.isUploading} tip={"上传中……"}>
<div ref={this.containerRef} />
</Spin>
);
}
}
2.2、fileMenu.js:
import uploadFile from './uploadFile';
import fileImg from '@/assets/img/file.png'; /**
* 扩展 上传附件的功能
editor: wangEdit的实例
editorSelector: wangEdit挂载点的节点
options: 一些配置
*/
export default (editor, editorSelector, options, changeUploading) => {
editor.fileMenu = {
init: function (editor, editorSelector) {
const div = document.createElement('div');
div.className = 'w-e-menu';
div.style.position = 'relative';
div.setAttribute('data-title', '附件');
const rdn = new Date().getTime();
div.onclick = function () {
document.getElementById(`up-${rdn}`).click();
} const input = document.createElement('input');
input.style.position = 'absolute';
input.style.top = '0px';
input.style.left = '0px';
input.style.width = '40px';
input.style.height = '40px';
input.style.zIndex = 10;
input.type = 'file';
input.name = 'file';
input.id = `up-${rdn}`;
input.className = 'upload-file-input'; div.innerHTML = `<span class="upload-file-span" style="position:absolute;top:0px;left:0px;width:40px;height:40px;z-index:20;background:#fff;"><img src=${fileImg} style="width:15px;margin-top:12px;" /></span>`;
div.appendChild(input);
editorSelector.getElementsByClassName('w-e-toolbar')[0].append(div); input.onchange = e => {
changeUploading(true);
// 使用uploadFile上传文件
uploadFile(e.target.files, {
uploadFileServer: options?.uploadFileServer, // 附件上传接口地址
maxFileSize: options?.maxFileSize, //限制附件最大尺寸
onOk: data => {
let aNode = '<p><a href=' + data.url + ' download=' + data.name + '>' + data.name + '</a></p>';
editor.txt.append(aNode);
changeUploading(false);
// editor.cmd.do(aNode, '<p>'+aNode+'</p>');
// document.insertHTML(aNode)
},
onFail: err => {
changeUploading(false);
console.log(err);
},
// 上传进度,后期可添加上传进度条
onProgress: percent => {
console.log(percent);
},
});
};
},
} // 创建完之后立即实例化
editor.fileMenu.init(editor, editorSelector)
}
2.3、uploadFile.js:
import { message } from 'antd'
/**
* 上传附件功能的实现
* @param {*} files
* @param {*} options
* @returns
*/
function uploadFile(files, options) {
if (!files || !files.length) {
return
}
let uploadFileServer = options?.uploadFileServer; //上传地址
const maxFileSize = options?.maxFileSize || 10;
const maxSize = maxFileSize * 1024 * 1024 //100M
const maxLength = 1; // 目前限制单次只可上传一个附件
const timeout = 1 * 60 * 1000 // 超时 1min
// ------------------------------ 验证文件信息 ------------------------------
const resultFiles = [];
for (let file of files) {
const name = file.name;
const size = file.size;
// chrome 低版本 name === undefined
if (!name || !size) {
options.onFail('');
return
}
if (maxSize < size) {
// 上传附件过大
message.warning('上传附件不可超过' + maxFileSize + 'M');
options.onFail('上传附件不可超过' + maxFileSize + 'M');
return
}
// 验证通过的加入结果列表
resultFiles.push(file);
}
if (resultFiles.length > maxLength) {
message.warning('一次最多上传' + maxLength + '个文件');
options.onFail('一次最多上传' + maxLength + '个文件');
return
}
// 添加附件数据(目前只做单文件上传)
const formData = new FormData()
formData.append('file', files[0]);
// ------------------------------ 上传附件 ------------------------------
if (uploadFileServer && typeof uploadFileServer === 'string') {
// 定义 xhr
const xhr = new XMLHttpRequest();
xhr.open('POST', uploadFileServer);
// 设置超时
xhr.timeout = timeout;
xhr.ontimeout = function () {
message.error('上传附件超时');
options.onFail('上传附件超时');
}
// 监控 progress
if (xhr.upload) {
xhr.upload.onprogress = function (e) {
let percent = void 0;
// 进度条
if (e.lengthComputable) {
percent = e.loaded / e.total;
console.log('上传进度:', percent);
if (options.onProgress && typeof options.onProgress === 'function') {
options.onProgress(percent);
}
}
}
}
// 返回数据
xhr.onreadystatechange = function () {
let result = void 0;
if (xhr.readyState === 4) {
if (xhr.status < 200 || xhr.status >= 300) {
// hook - error
if (options.onFail && typeof options.onFail === 'function') {
options.onFail(result);
}
message.error('上传失败');
return;
}
result = xhr.responseText
if ((typeof result === 'undefined' ? 'undefined' : typeof result) !== 'object') {
try {
result = JSON.parse(result);
} catch (ex) {
// hook - fail
if (options.onFail && typeof options.onFail === 'function') {
options.onFail(result);
}
message.error('上传失败');
return;
}
}
const res = result || []
if (res?.code == 200) {
options.onOk && options.onOk(res.data);
}
}
}
// 自定义 headers
xhr.setRequestHeader('token', sessionStorage.getItem('token'));
// 跨域传 cookie
xhr.withCredentials = false;
// 发送请求
xhr.send(formData);
}
}
export default uploadFile
3、使用富文本编辑器editor组件
在首页Home.jsx里测试使用editor组件,在这里,演示在同一个页面使用多个editor组件,还是直接上代码:
3.1、Home.jsx:
import React, { createRef } from "react";
import { connect } from 'react-redux';
import { Button } from 'antd';
import Editor from '@/components/common/editor';
class Home extends React.Component {
constructor(props) {
super(props);
this.editorRefSingle = createRef();
this.state = {
editorList: []
}
}
componentDidMount() {
let list = [
{ id: 1, content: '<p>初始化内容1</p>' },
{ id: 2, content: '<p>初始化内容2</p>' },
{ id: 3, content: '<p>初始化内容3</p>' }
];
list.forEach(item => {
this['editorRef' + item.id] = createRef();
})
this.setState({
editorList: list
})
}
// 获取内容(数组多个editor)
getEditorContent = (item) => {
let editorHtml = this['editorRef' + item.id].current.editor.txt.html();
console.log('从多个中获取一个:', editorHtml, item);
}
// 获取内容(单个editor)
getEditorContentSingle = () => {
let editorHtml = this.editorRefSingle.current.editor.txt.html();
console.log('获取单个:', editorHtml);
}
render() {
return (
<div className="main-container home" style={{ margin: 0, height: '100%' }}>
{/* editor的测试demo */}
<div style={{paddingBottom:10}}>
<h2>根据数组循环生成多个editor,ref需要动态定义</h2>
{
this.state.editorList.map((item) => (
<div className="mb_20" key={item.id}>
<Editor
ref={this['editorRef' + item.id]}
isUploadFile={true}
defaultHtml={item.content}
uploadFileServer="http://rap2api.taobao.org/app/mock/297868/libo/test1/uploadEditorFile"
maxFileSize={10}
uploadImgServer="http://rap2api.taobao.org/app/mock/297868/libo/test1/uploadEditorImg"
maxImgSize={2}
/>
<Button onClick={() => this.getEditorContent(item)}>获取内容</Button>
</div>
))
}
<h2>单个editor</h2>
<div className="mb_20">
<Editor
ref={this.editorRefSingle}
isUploadFile={true}
defaultHtml="<p>初始化内容哈哈哈</p>"
height={100}
uploadFileServer="http://rap2api.taobao.org/app/mock/297868/libo/test1/uploadEditorFile"
maxFileSize={5}
uploadImgServer="http://rap2api.taobao.org/app/mock/297868/libo/test1/uploadEditorImg"
maxImgSize={2}
menus={['head', // 标题
'bold', // 粗体
'fontSize', // 字号
'fontName', // 字体
'italic', // 斜体
'underline', // 下划线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'image', // 插入图片
'table', // 表格
]}
/>
<Button onClick={this.getEditorContentSingle}>获取内容</Button>
</div>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
userInfo: state.userInfo.user,
menuC: state.userInfo.menuC,
}
}
export default connect(mapStateToProps, {
})(Home);
4、效果



备注:代码里的上传图片和上传附件的接口地址是维护在rap2上的mock数据。根据需要改成自己的真实接口即可。
React项目中使用wangeditor以及扩展上传附件菜单的更多相关文章
- 【React踩坑记四】React项目中引入并使用js-xlsx上传插件(结合antdesign的上传组件)
最近有一个前端上传并解析excel/csv表格数据的需求. 于是在github上找到一个14K star的前端解析插件 github传送门 官方也有,奈何实在太过于浅薄.于是做了以下整理,避免道友们少 ...
- kindeditor在Java项目中的应用以及图片上传配置
在官网下载Kindededitor的开发包 在项目中javaweb项目中导入kindeditor必须要使用的Jar包(用于文件上传,除非你的富文本编辑器不使用图片上传)jar包可以在官网的开发包中 ...
- 在项目中全局添加FastClick导致图片上传插件在ios端失效的解决方案
---恢复内容开始--- 项目是移动端的项目,为了解决300ms的click延迟,所以在全局中加入了FastClick,引入的方式很简单,网上一大堆教程,这里不做赘述 我们就谈,我遇到的问题: 某天产 ...
- 一个项目中哪些文件是要上传到 git上的,哪些是不必要的
- 在vue项目中使用element-ui的Upload上传组件
<el-upload v-else class='ensure ensureButt' :action="importFileUrl" :data="upLoadD ...
- java上传附件含有%处理或url含有%(URLDecoder: Illegal hex characters in escape (%) pattern - For input string)
在附件名称中含有%的时候,上传附件进行url编码解析的时候会出错,抛出异常: Exception in thread "main" java.lang.IllegalArgumen ...
- 如何在非 React 项目中使用 Redux
本文作者:胡子大哈 原文链接:https://scriptoj.com/topic/178/如何在非-react-项目中使用-redux 转载请注明出处,保留原文链接和作者信息. 目录 1.前言 2. ...
- React项目中使用Mobx状态管理(二)
并上一节使用的是普通的数据状态管理,不过官方推荐使用装饰器模式,而在默认的react项目中是不支持装饰器的,需要手动启用. 官方参考 一.添加配置 官方提供了四种方法, 方法一.使用TypeScrip ...
- 深入浅出TypeScript(5)- 在React项目中使用TypeScript
前言 在第二小节中,我们讨论了利用TypeScript创建Web项目的实现,在本下节,我们讨论一下如何结合React创建一个具备TypeScript类型的应用项目. 准备 Webpack配置在第二小节 ...
随机推荐
- 仿真pda,部署时出现问题
为什么部署到基于 Windows Mobile 的 Pocket PC 设备或模拟器会因共享冲突错误而失败 自己遇到了网上找到的解决方案http://hi.baidu.com/yeflower/blo ...
- 论文解读(GMI)《Graph Representation Learning via Graphical Mutual Information Maximization》
Paper Information 论文作者:Zhen Peng.Wenbing Huang.Minnan Luo.Q. Zheng.Yu Rong.Tingyang Xu.Junzhou Huang ...
- 网络编程 --安装wkhtmltopdf出现中文乱码的情况
1 首先下载安装包 2安装依赖文件apt-get install libxfont1 xfonts-encodings xfonts-utils xfonts-base xfonts-75dpi su ...
- idea 下 Vue
一.需要了解的基本知识 node.js Node.js是一个Javascript运行环境(runtime),发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进行了封装.N ...
- 你了解过Servlet3.0吗?
Servlet3.0相对于Servlet2.0来说最大的改变是引入了Annotation标注来取代xml配置,用于简化web应用的开发和部署.最主要几项特性: 1. 新增的注解支持:该版本新增了若干注 ...
- Spring Boot 2.X 有什么新特性?与 1.X 有什么区别?
配置变更JDK 版本升级第三方类库升级响应式 Spring 编程支持HTTP/2 支持配置属性绑定更多改进与加强-
- 阿里云删除mysql
记录以下,学生买的轻量级服务器安装mysql之后发现没有初始密码,之后在被自己七搞八搞后彻底歇菜,就准备重新卸载安装 记录自己卸载过程首先运行rpm -qa | grep -i mysql出来的是:m ...
- Ribbon负载均衡能干什么?
(1)将用户的请求平摊的分配到多个服务上 (2)集中式LB即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至 ...
- 谈一谈 Kafka 的再均衡?
在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义 ...
- JVM内存模型小结
JVM运行时的数据区域划分图如下,该图是JVM内存模型最主要的内容. 从图中可以看出来,JVM将内存主要划分为五个部分:程序计数器.Java虚拟机栈.本地方法栈.Java堆和方法区.这些被划分为用途不 ...