当前位置: 首页 > 工具软件 > react-cursor > 使用案例 >

react-quill 图片上传及图片粘贴功能踩坑记录

秦诚
2023-12-01

Gitlab React-quill:https://github.com/zenoamaro/react-quill

中文文档 Quill:http://doc.quilljs.cn/1409381

官网 Quill:https://quilljs.com/docs/quickstart/

Gitlab Delta:https://github.com/quilljs/delta

需求:

  1. 图片上传由base64改为走后端接口存储服务端
  2. 图片粘贴功能,也走后台接口存储服务端

坑点:

  1. focus,onblur与react-quill的图片上传有冲突导致死循环

一开始的需求是focus点击变成只读状态,onBlur后变成可写状态并调接口保存,查阅文档后用了编辑器自带的

readOnly字段,在没加图片上传的imageHandler的时候还是好好的,但是一加就陷入了一直调保存接口死循环,中间有试图绕过但又中了其他坑最后和产品商量放弃了onBlur去保存采用按钮手动保存

  1. imageHandler在配合onChange方法时会出现输一个单词就失去焦点的问题

这个问题的使用场景是在配合antd form表单时出现,研究了很久发现不配合form的时候也会出现,然后一点点试发现只要是handler里的使用方法都会出现这个问题不只是imageHandler,官方文档的介绍也很少,最后是在React Quill 富文本编辑器中图片上传服务端找到了解决方案,使用useMemo包裹

const modules = React.useMemo(() => ({
    toolbar: {
        container: toolbarContainer,
        handlers: {
            image: imageHandler
        }
    },
}), []);
  1. react-quill setEditorContents使用报错

需要用该方法对编辑器做初始化,但使用时出现了undefined的错误,因为网上没找着具体的函数使用说明所以就以为第一个参数是传内容值,后来点进源码发现第一个参数要传quill的实例,第二个参数才是内容值

reactQuillRef?.current?.setEditorContents?.(
	reactQuillRef?.current?.getEditor(),
    data,
);
  1. 在imageHandler里拿不到ref为null

因为要在imageHandler里调上传接口所以需要获取编辑器的数据,但ref为空了,后来定位发现是ref的使用有问题

// 正确
let reactQuillRef: any = useRef(null);
<ReactQuill
	ref={reactQuillRef}
/>
        
// 错误
let reactQuillRef: any = useRef(null);
<ReactQuill
	ref={e=>{reactQuillRef = e}}
/>        
  1. 粘贴的时候失去焦点取不到光标的定位

原本想粘贴图片的时候去获取光标的位置然后粘在光标处,但始终拿不到光标定位,可能是因为粘贴的时候调用了上传图片的接口然后函数中执行了input.click方法导致失焦,后来直接写死了个最大值然后它就自动定位到末尾了

quill.insertEmbed(9999, 'image', link)
  1. setEditorContents 如果有图片也会莫名其妙的触发粘贴事件

在配合form编辑时初始化表单使用form.setFields发现先触发了粘贴的函数,然后报错导致整个页面挂了,一点点调试后发现用插件的函数setEditorContents也同样会触发,但是在readOnly=true时是正常的,最后实在没有找到方法只能在编辑的时候放弃使用粘贴的功能

Base64转文件流

粘贴的时候需要将base格式转为文件流,采用了以下博主的方法

React-Quill中的图片上传及显示

convertBase64UrlToBlob = (urlData) => {
    //去掉url的头,并转换为byte
    const bytes = window.atob(urlData.split(',')[1]);
    //处理异常,将ascii码小于0的转换为大于0
    const ab = new ArrayBuffer(bytes.length);
    const ia = new Uint8Array(ab);
    ia.forEach((i, index) => {
      ia[index] = bytes.charCodeAt(index);
    });
    return new Blob([ia], { type: urlData.split(',')[0].split(':')[1].split(';')[0] });
};

完整代码:

import React, {
  forwardRef,
  useImperativeHandle,
  useRef,
} from 'react';
import 'react-quill/dist/quill.snow.css';
import ReactQuill from 'react-quill';
import styles from './index.less';
import { connect } from 'dva';
import Utils from '@/utils/utils';

const formats = [
  'header',
  'bold',
  'italic',
  'underline',
  'strike',
  'blockquote',
  'code-block',
  'list',
  'bullet',
  'indent',
  'link',
  'image',
  'color',
  'background',
  'font',
  'align',
  'clean',
];

const Editor = (props: any) => {
  const {
    refInstance,
    readOnly = false,
    closeClipboardImg = false,
    ...otherProps
  } = props;

  let reactQuillRef: any = useRef(null);

  // 上传图片
  const imageHandler = async () => {
    const input = document.createElement('input');
    input.setAttribute('type', 'file');
    input.setAttribute('accept', 'image/*');
    input.setAttribute('multiple', 'multiple');
    input.click();
    input.onchange = async () => {
      Array.from(input.files).forEach((item) => {
        const formData = new FormData();
        formData.append('files', item);
        // 上传图片
        props.dispatch({
          type: 'attachment/addAttachment',
          payload: formData,
          callback: (url: any) => {
            // 获取url
            let quill = reactQuillRef?.current?.getEditor(); //获取到编辑器本身
            const cursorPosition = quill.getSelection().index; //获取当前光标位置
            const link = url;
            quill.insertEmbed(cursorPosition, 'image', link); //插入图片
            quill.setSelection(cursorPosition + 1); //光标位置加1
          },
        });
      });
    };
  };

  // 对外暴露
  useImperativeHandle(refInstance, () => ({
    focus: (param = {}) => {
      reactQuillRef?.current?.focus?.();
    },
    getEditorContents: (param = {}) => {
      return reactQuillRef?.current?.getEditorContents?.();
    },
    setEditorContents: (data: any) => {
      reactQuillRef?.current?.setEditorContents?.(
        reactQuillRef?.current?.getEditor(),
        data,
      );
    },
    moveMouseEnd: () => {
      // 将光标移动到最后
      let editor = reactQuillRef?.current?.getEditor();
      editor.setSelection(9999, 0);
    },
  }));

  // 粘贴图片
  const customImgForPaste = async (node: any, delta: any) => {
    delta.forEach((op: any) => {
      let file = Utils.base64toFile(op?.insert?.image);
      const formData = new FormData();
      formData.append('files', file);
      // 上传图片
      props.dispatch({
        type: 'attachment/addAttachment',
        payload: formData,
        callback: (url: any) => {
          // 获取url
          let quill = reactQuillRef?.current?.getEditor(); //获取到编辑器本身
          const cursorPosition = 99999; //粘贴的时候失去焦点拿不到光标位置
          const link = url;
          quill.insertEmbed(cursorPosition, 'image', link); //插入图片
          quill.setSelection(cursorPosition); //光标位置
        },
      });
    });
  };

  const toolbar = React.useMemo(
    () => ({
      toolbar: {
        container: [
          [{ header: [1, 2, 3, 4, 5, 6, false] }],
          ['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block'],
          [
            { list: 'ordered' },
            { list: 'bullet' },
            { indent: '-1' },
            { indent: '+1' },
          ],
          ['link', 'image'],
          [{ color: [] }, { background: [] }, { align: [] }],
          ['clean'],
        ],
        handlers: {
          image: imageHandler,
        },
      },
      clipboard: {
        matchers: [
          // 粘贴事件和setEditorContents有冲突报错,但在readOnly=true的时候可以正常使用
          // 临时处理方式:需要setEditorContents的地方先将readOnly=true再使用,或者先去掉粘贴功能比如表单编辑的时候
          closeClipboardImg ? [] : ['img', customImgForPaste],
        ],
      },
      // imageDrop: true,
    }),
    [],
  );

  const defaultProps = {
    theme: 'snow',
    modules: toolbar,
    formats,
    placeholder: '请输入',
    className: styles.default_style,
  };

  const readOnlyProps = readOnly
    ? {
        readOnly: true,
        modules: {},
        formats: {},
        theme: '',
      }
    : {};

  return (
    <>
      <ReactQuill
        ref={reactQuillRef}
        {...defaultProps}
        {...otherProps}
        {...readOnlyProps}
      />
    </>
  );
};

const Ed = connect()(Editor);

export default forwardRef((props, ref) => <Ed {...props} refInstance={ref} />);

 类似资料: