react 项目结合 braft-editor 实现精致的富文本组件

简介: 基于 react 的富文本组件本来就不多,braft-editor 是 react 里人气相当高的富文本,这里基于他打造一个精致的富文本。

项目主要依赖:(先安装,步骤略)

create-react-app:3.0.0

1
{
2
  "react": "16.8.6",
3
  "react-router-dom": "5.0.0",
4
  "antd": "^3.19.2",
5
  "axios": "^0.19.0",
6
  "braft-editor": "^2.3.6",
7
  "braft-polyfill": "^0.0.1",
8
  "prop-types": "^15.7.2"
9
}

1.组件

文件目录

在这里插入图片描述

src/components/AppEditor/index.jsx

1
import React, { Component } from "react";
2
// 添加ie10支持
3
import "braft-polyfill";
4
import BraftEditor from "braft-editor";
5
import "braft-editor/dist/index.css";
6
import PropTypes from "prop-types";
7
import axios from "axios";
8
import { message } from "antd";
9
10
import "./style.less"; // 见下文
11
import config from "./config"; // 见下文
12
import mediaBaseconfig from "./media"; // 见下文
13
import {
14
  uploadHtml,
15
  uploadImage,
16
  maxFileSize,
17
  isImageFile
18
} from "@/utils/upload"; // 见下文 uploadHtml和uploadImage是定义的上传文件到后端的接口,根据自己的项目更换
19
20
class AppEditor extends Component {
21
  state = {
22
    // 创建一个空的editorState作为初始值
23
    editorState: BraftEditor.createEditorState(null)
24
  };
25
26
  static propTypes = {
27
    // 富文本初始内容
28
    html: PropTypes.string,
29
    //  限制图片文件大小
30
    fileMaxSize: PropTypes.number
31
  };
32
33
  // 默认的props
34
  static defaultProps = {
35
    fileMaxSize: 2 // 单位默认是Mb,
36
  };
37
38
  componentWillUnmount() {
39
    this.setState = (state, callback) => {
40
      return;
41
    };
42
  }
43
44
  // 初始化编辑器内容
45
  async componentWillReceiveProps(props) {
46
    try {
47
      const { html } = props;
48
      const { data } = await axios.get(html);
49
      this.setState({
50
        editorState: BraftEditor.createEditorState(data)
51
      });
52
      return data;
53
    } catch (error) {
54
      console.log(error);
55
    }
56
  }
57
58
  // 子组件上传方法(供父组件调用)
59
  handleSubmitContent = async () => {
60
    const htmlContent = this.state.editorState.toHTML();
61
    try {
62
      const url = await uploadHtml(htmlContent);
63
      return url;
64
    } catch (error) {
65
      console.log(error);
66
    }
67
  };
68
69
  // 监听编辑器内容变化同步内容到state
70
  handleEditorChange = editorState => {
71
    this.setState({ editorState });
72
  };
73
74
  // 媒体上传校验
75
  mediaValidate = file => {
76
    const { fileMaxSize } = this.props;
77
78
    // 类型限制
79
    if (!isImageFile(file)) {
80
      return false;
81
    }
82
83
    // 大小限制
84
    if (fileMaxSize && !maxFileSize(file, fileMaxSize)) {
85
      return false;
86
    }
87
88
    return true;
89
  };
90
91
  // 媒体上传
92
  mediaUpload = async ({ file, success, error }) => {
93
    message.destroy();
94
    try {
95
      const url = await uploadImage(file);
96
      success({ url });
97
      message.success("上传成功");
98
    } catch (err) {
99
      error(err);
100
      message.error(err.message || "上传失败");
101
    }
102
  };
103
104
  render() {
105
    const { editorState } = this.state;
106
107
    // 媒体配置
108
    const media = {
109
      // 上传校验
110
      validateFn: this.mediaValidate,
111
      // 上传
112
      uploadFn: this.mediaUpload,
113
      // 基本配置
114
      ...mediaBaseconfig
115
    };
116
117
    return (
118
      <div
119
        className="custom-editor"
120
        style={{ marginTop: 20, ...this.props.style }}
121
      >
122
        <BraftEditor
123
          style={{ height: 600, overflow: "hidden" }}
124
          {...config}
125
          media={media}
126
          value={editorState}
127
          onChange={this.handleEditorChange}
128
        />
129
      </div>
130
    );
131
  }
132
}
133
134
export default AppEditor;

src/components/AppEditor/config.js

1
const config = {
2
  placeholder: "请输入内容",
3
4
  // 编辑器工具栏的控件列表
5
  controls: [
6
    "headings",
7
    "font-size",
8
    "letter-spacing",
9
    "separator", // 分割线
10
11
    "text-color",
12
    "bold",
13
    "italic",
14
    "underline",
15
    "strike-through",
16
    "remove-styles",
17
    "separator",
18
    "text-align",
19
    "separator",
20
    "emoji",
21
    "media"
22
  ],
23
24
  // 字号配置
25
  fontSizes: [12, 14, 16, 18, 20, 24, 28, 30, 32, 36],
26
27
  // 图片工具栏的可用控件
28
  imageControls: [
29
    "float-left", // 设置图片左浮动
30
    "float-right", // 设置图片右浮动
31
    "align-left", // 设置图片居左
32
    "align-center", // 设置图片居中
33
    "align-right", // 设置图片居右
34
    "link", // 设置图片超链接
35
    "size", // 设置图片尺寸
36
    "remove" // 删除图片
37
  ]
38
};
39
export default config;

src/components/AppEditor/media.js

1
const mediaBaseconfig = {
2
  // 文件限制
3
  accepts: {
4
    image: "image/png,image/jpeg,image/gif,image/webp,image/apng,image/svg",
5
    video: false,
6
    audio: false
7
  },
8
9
  //   允许插入的外部媒体的类型
10
  externals: {
11
    // 是否允许插入外部图片,
12
    image: false,
13
    //    是否允许插入外部视频,
14
    video: false,
15
    //    是否允许插入外部视频,
16
    audio: false,
17
    //    是否允许插入嵌入式媒体,例如embed和iframe标签等,
18
    embed: false
19
  }
20
};
21
22
export default mediaBaseconfig;

src/components/AppEditor/style.less

1
:global(.custom-editor .bf-controlbar) {
2
  background-color: #fff;
3
}
4
5
:global(.custom-editor .bf-container) {
6
  border: 1px solid #c5c5c5;
7
}
8
9
:global(.custom-editor .bf-content) {
10
  min-height: 600px;
11
}

src/utils/upload.js

1
import { message } from "antd";
2
3
/**
4
 *
5
 * @param {file} file 源文件
6
 * @desc 限制为图片文件
7
 * @retutn 是图片文件返回true否则返回false
8
 */
9
10
export const isImageFile = (file, fileTypes) => {
11
  const types = fileTypes || [
12
    "image/png",
13
    "image/gif",
14
    "image/jpeg",
15
    "image/jpg",
16
    "image/bmp",
17
    "image/x-icon",
18
    "image/webp",
19
    "image/apng",
20
    "image/svg"
21
  ];
22
23
  const isImage = types.includes(file.type);
24
  if (!isImage) {
25
    message.error("上传文件非图片格式!");
26
    return false;
27
  }
28
29
  return true;
30
};
31
32
/**
33
 *
34
 * @param {file} file 源文件
35
 * @param {number} fileMaxSize  图片限制大小单位(MB)
36
 * @desc 限制为文件上传大小
37
 * @retutn 在限制内返回true否则返回false
38
 */
39
40
export const maxFileSize = (file, fileMaxSize = 2) => {
41
  const isMaxSize = file.size / 1024 / 1024 < fileMaxSize;
42
  if (!isMaxSize) {
43
    message.error("上传头像图片大小不能超过 " + fileMaxSize + "MB!");
44
    return false;
45
  }
46
  return true;
47
};
48
49
/**
50
 *
51
 * @param {file} file 源文件
52
 * @desc 读取图片文件为base64文件格式
53
 * @retutn 返回base64文件
54
 */
55
// 读取文件
56
export const readFile = file => {
57
  return new Promise((resolve, reject) => {
58
    const reader = new FileReader();
59
    reader.onload = e => {
60
      const data = e.target.result;
61
      resolve(data);
62
    };
63
    reader.onerror = () => {
64
      const err = new Error("读取图片失败");
65
      reject(err.message);
66
    };
67
68
    reader.readAsDataURL(file);
69
  });
70
};
71
72
/**
73
 *
74
 * @param {string} src  图片地址
75
 * @desc 加载真实图片
76
 * @return 读取成功返回图片真实宽高对象 ag: {width:100,height:100}
77
 */
78
79
export const loadImage = src => {
80
  return new Promise((resolve, reject) => {
81
    const image = new Image();
82
    image.src = src;
83
    image.onload = () => {
84
      const data = {
85
        width: image.width,
86
        height: image.height
87
      };
88
      resolve(data);
89
    };
90
    image.onerror = () => {
91
      const err = new Error("加载图片失败");
92
      reject(err);
93
    };
94
  });
95
};
96
97
/**
98
 *
99
 * @param {file} file 源文件
100
 * @param {object} props   文件分辨率的宽和高   ag: props={width:100, height :100}
101
 * @desc  判断图片文件的分辨率是否在限定范围之内
102
 * @throw  分辨率不在限定范围之内则抛出异常
103
 *
104
 */
105
export const isAppropriateResolution = async (file, props) => {
106
  try {
107
    const { width, height } = props;
108
    const base64 = await readFile(file);
109
    const image = await loadImage(base64);
110
    if (image.width !== width || image.height !== height) {
111
      throw new Error("上传图片的分辨率必须为" + width + "*" + height);
112
    }
113
  } catch (error) {
114
    throw error;
115
  }
116
};
117
118
// 上传图片 根据自己项目更换
119
export const uploadImage = file => {
120
  return new Promise((resolve, reject) => {});
121
};
122
123
// 上传html  根据自己项目更换
124
export const uploadHtml = editorContent => {
125
  return new Promise((resolve, reject) => {});
126
};

2.使用

1
import AppEditor from "@/components/AppEditor";
2
import React, { Component } from "react";
3
export default class HotelSetting extends Component {
4
  state = {
5
    html: ""
6
  };
7
8
  componentDidMount() {
9
    this.setState({
10
      html:
11
        "https://hotel-1259479103.cos.ap-chengdu.myqcloud.com/admin/html/r2yyo8dyjrla71bn8qm5g-2019-07-11-10-34-19.html"
12
    });
13
  }
14
15
  handleSubmit = async () => {
16
    try {
17
      const res = await this.editor.handleSubmitContent();
18
      console.log(res);
19
    } catch (error) {
20
      console.log(error);
21
    }
22
  };
23
24
  render() {
25
    const { html } = this.state;
26
    return (
27
      <AppEditor
28
        html={html}
29
        style={{ marginLeft: 100 }}
30
        ref={e => {
31
          this.editor = e;
32
        }}
33
      />
34
    );
35
  }
36
}

3.使用效果

在这里插入图片描述

在这里插入图片描述