相关文章推荐
开朗的皮带  ·  org.springframework.bo ...·  2 月前    · 
笑点低的橙子  ·  django filter - CSDN文库·  4 月前    · 
React使用Quill 富文本自定义格式刷功能&复制图片&采坑

[原文地址]( www.yuque.com/docs/share/… 《react-quill 富文本编辑器&采坑》)

由于业务功能,需要实现当用户在富文本里进行可以格式化操作,内容粘贴操作的时候,如果用户复制的是图片,需要将图片上传服务器后,插入到文本内;看似合情合理的要求,却有很多坑。

安装 react-quill

npm install react-quill @ beta

github地址

quill 中文文档地址

quill 文档地址

直接上手练练 Codeopen

1、自定义富文本格式刷功能

quill是不具备格式刷的功能,那么也就没有对应的icon,这时就需要去自定义工具栏,quill工具栏控件即可以用一个格式名称数组定义,也可以用一般的HTML容器定义。这里采用的是HTML容器定义。

先自定义一个格式刷的icon。这里的icon采用的是iconfont的unicode引用,支持按字体的方式去动态调整图标大小,颜色等等。可以实现选中高亮的效果,与原来的工具栏交互保持一致。

const CustomButton = () => <i className="iconfont">&#xe608;</i>;

自定义HTML容器CustomToolbar

const CustomToolbar = () => (
  <div id="toolbar">
    <select className="ql-header" defaultValue={''} onChange={(e) => e.persist()}>
      <option value="1" />
      <option value="2" />
      <option value="3" />
      <option value="4" />
      <option value="5" />
      <option selected />
    </select>
    <select className="ql-size" defaultValue={''} onChange={(e) => e.persist()}>
      <option value="small" />
      <option selected />
      <option value="large" />
      <option value="huge" />
    </select>
    <button className="ql-formatBrush">
      <CustomButton />
    </button>
    <button className="ql-bold" />
    <button className="ql-italic" />
    <button className="ql-underline" />
    <button className="ql-strike" />
    <select className="ql-color" />
    <select className="ql-background" />
    <select className="ql-align" />
    <button className="ql-list" value="ordered" />
    <button className="ql-list" value="bullet" />
    <button className="ql-indent" value="+1" />
    <button className="ql-indent" value="-1" />
    <button className="ql-image" />
    <button className="ql-clean" />

CustomToolbar就是加入原有的工具栏与格式刷工具栏。把自定义的工具栏放到return里。

return (
    <div className={styles['rich-text']}>
      <CustomToolbar />
      <ReactQuill
        ref={editorRef}
        placeholder={placeholder || '请输入内容'}
        modules={modules}
        theme="snow"
        {...props}
        value={value}

格式刷功能

接下来是格式刷功能,在modules完成格式刷。formatBrush与工具栏 className="ql-formatBrush" 保持一致。

//记得声明变量
let quillEditor = null;
const copyFormatting = {
  value: 'un-active',
  format: {},
//modules 方法
const modules = useMemo(() => {
    return {
      toolbar: {
        // container: toolbarContainer,
        container: '#toolbar',
        handlers: {
          image: imageHandler,
          formatBrush: () => {
            copyFormatBrush(quillEditor, copyFormatting);
  }, []);

关键的格式设置样式方法,点击格式刷,如果选中的区域有样式,则保存样式,格式刷功能为选中状态,再次点击删除样式,取消选中;设置格式样式。可以把这3个方法放到单独文件导出管理。

/* eslint-disable @typescript-eslint/no-use-before-define */
export const copyFormatBrush = (quillEditor, copyFormatting) => {
  // 点击格式刷,如果有选中区域且有样式,则保存其样式,按键状态改为选中。
  // 再次点击,删除样式,按键取消选中。
  if (copyFormatting.value === 'un-active') {
    const range = quillEditor.getSelection(true);
    if (range == null || range.length === 0) return;
    const format = quillEditor.getFormat(range);
    if (Object.keys(format).length === 0) return;
    setCopyFormatting(quillEditor, copyFormatting, 'active', format);
  } else {
    setCopyFormatting(quillEditor, copyFormatting, 'un-active', null);
// 设置copyFormatting: 修改保存的样式、按键状态、粘贴样式的处理程序
export const setCopyFormatting = (quill, copyFormatting, value, format) => {
  copyFormatting.value = value;
  copyFormatting.format = format;
  const toolbar = quill.getModule('toolbar').container;
  const brushBtn = toolbar.querySelector('.ql-formatBrush');
  if (value === 'active') {
    brushBtn.classList.add('ql-formatBrushactive');
    quill.on('selection-change', pasteFormatHandler);
  } else {
    brushBtn.classList.remove('ql-formatBrushactive');
    quill.off('selection-change', pasteFormatHandler);
  function pasteFormatHandler(range, oldRange, source) {
    return pasteFormat(range, oldRange, source, quill, copyFormatting);
// 粘贴样式的处理程序: 如果选中范围且有保存样式,则粘贴样式,并初始化copyFormatting
export const pasteFormat = (range, oldRange, source, quill, copyFormatting) => {
  if (range && copyFormatting.format) {
    if (range.length === 0) {
    } else {
      quill.formatText(range.index, range.length + 1, copyFormatting.format);
      setCopyFormatting(quill, copyFormatting, 'un-active', null);
  } else {
    // console.log('Cursor not in the editor')

本人测试使用默认的 ql-active 高亮样式,不起作用,只有自定义格式刷的高亮样式:

  .ql-toolbar.ql-snow .ql-formatBrushactive {
      color: #06c;

以上格式刷功能已经实现。

2、内容粘贴操作-图片直接预览

用户复制的内容里包含跨域的图片,需要将图片上传服务器后,插入到文本内显示。

目前image功能是自定义封装了,用户插入图片会走antd的Upload上传,把图片资源上传服务器后再插入我们本服务器的URL图片。

自定义Quill的行为和功能

主要方法是imageHandler,使用 Quill.register 模块可以自定义Quill的行为和功能,

const modules = useMemo(() => {
    return {
      toolbar: {
        // container: toolbarContainer,
        container: '#toolbar',
        handlers: {
          image: imageHandler,
  }, []);

具体的弹窗逻辑就不过多展示,在拿到上传完的fileList数据,就在富文本插入图片。

//在index.jsx内引入自定义的模块image功能
import './ImageBlot';
// 插入图片
  const insertImages = (fileList = []) => {
    if (quillEditor && typeof quillEditor.insertEmbed === 'function') {
      quillEditor.focus();
      const unprivilegedEditor = editorRef.current.makeUnprivilegedEditor(quillEditor);
      fileList.forEach((file) => {
        setTimeout(() => {
          const range = unprivilegedEditor.getSelection();
          const position = range ? range.index : 0;
          quillEditor.insertEmbed(position, 'image', {
            ossid: file.ossId,
            url: file.url,
          quillEditor.setSelection(position + 1, 0);
        }, 0);
// return 
<ImageUploadModal
    onCancel={() => {
      setShowImageUpload(false);
    onCreate={(fileList) => {
      setShowImageUpload(false);
      insertImages(fileList);

复制内容图片的上传拿到新的本服务器的url数据也是在自定义image模块内实现。

import { Quill } from 'react-quill';
import { getTmpUrl } from '@/components/AliyunOSSUpload/service';
import { isExpired } from '@/components/AliyunOSSUpload/utils';
import { saveAliOssImg } from '@/services/global';
import errorImg from './error-img.png';
const BlockEmbed = Quill.import('blots/block/embed');
const getExpireFromUrl = (url) => {
  const index = url.indexOf('?');
  if (index !== -1) {
    const reg = new RegExp('(^|&)Expires=([^&]*)(&|$)', 'i');
    const str = url.substring(index + 1, url.length);
    const expire = str.match(reg)[2];
    return expire || 0;
  return 0;
const replacedImage = (ossId, node) => {
  getTmpUrl({ ossIds: [ossId] }).then((data) => {
    if (data && data.length > 0 && data[0].tmpUrl) {
      const url = data[0].tmpUrl;
      node.setAttribute('src', url);
 * 自定义图片标签,增加ossId参数
class ImageBlot extends BlockEmbed {
  static create(value) {
    console.log('ImageBlot :>> ', value);
    const node = super.create();
    if (value.ossid && value.url) {
      node.setAttribute('ossid', value.ossid);
      node.setAttribute('src', value.url);
      const expire = getExpireFromUrl(value.url);
      if (expire && isExpired(parseInt(expire, 10))) {
        replacedImage(value.ossid, node);
    //对URL图片资源进行装换
    if (value.url) {
      let sourceType = 'URL';
      let content = value.url;
      if (value.url.indexOf('base64') > -1) {
        sourceType = 'BASE64';
        content = value.url.split('base64,')[1];
      // 跨域图片装换为本服务器资源url
      saveAliOssImg({ sourceType, content })
        .then((data) => {
          if (data && data.url) {
            node.setAttribute('src', data.url);
        .catch(() => {
          node.setAttribute('src', errorImg);
   console.log('ImageBlot --node-:>> ', node);
    return node;
  static value(node) {
    const ossId = node.getAttribute('ossid');
    const url = node.getAttribute('src');
    return {
      ossid: ossId,
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';
Quill.register(ImageBlot);

1、复制域外资源:

2、粘贴富文本效果:

从输出看最后替换结果是没有问题

加入图片加载失败、本地路径图片资源加载失败的处理,使用图片占位符。

1、“直接复制本地文件夹内的图片,在mac下ok,在windows下失效”,找了下原来windows下复制出来的是bolb流(后端服务器接受失败),而mac下是file流。进一步探索发现在windows文件夹系统中,复制文本类的东西,是在剪切板中,可以获得之;但是,复制的图片文件,不论是右键复制,还是Ctrl + C复制都不行。目前,windows复制图片可以选择富文本的插入图片功能实现。(欢迎留言讨论)

2、粘贴后页面滚到底部,解决方案如下:

less文件里添加如下样式。

.ql-clipboard {
      position: fixed;
      // display: none;
      left: 50%;
      top: 50%;

欢迎多多留言讨论,有好的方案可以留下宝贵意见。

分类:
前端
标签:
  •