Cropper.js + React 实现前端图片裁剪上传

    前言:记录一次使用cropper.js+react实现图片裁剪上传的过程、源码、遇到的bug和解决方案。
    
github地址icon-default.png?t=M666https://github.com/fengyuanchen/cropperjs
官网地址icon-default.png?t=M666https://fengyuanchen.github.io/cropperjs/
官网示例地址icon-default.png?t=M666https://fengyuanchen.github.io/photo-editor/

开发过程:

一、cropper.js的引入

import 'cropperjs/dist/cropper.css';
import CropperJs from 'cropperjs';

        注:css及js均需引入

二、创建img元素并设置样式

const ref = useRef();

<div className={styles.cropper}>
    <img ref={ref} alt=""/>
</div>

.cropper {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 60%;
  margin-bottom: 20px;
  img {
    max-width: 100%;
    max-height: 100%;
  }
}

        注:img将作为用于裁剪的目标图像或画布元素,你可以预设src作为目标图像,或者也可以通过replace(url[, hasSameSize])去替换更新目标图像。
    另外,cropper.js将挂载在img的父级元素下。所以你需要将img放置在块级元素下,并设置img的最大宽高。

三、初始化实例并根据文档及需求配置你的options

const [cropper, setCropper] = useState(); // 存储cropper对象
useEffect(() => {
  const myCropper = new CropperJs(ref.current, {
    viewMode: 1,
    dragMode: 'move',
    aspectRatio: LONG / WIDE,
    autoCropArea: 0.9,
    highlight: false,
    cropBoxResizable: false,
    toggleDragModeOnDblclick: false,
  });
  setCropper(myCropper);
}, [])

 四、使用Upload用于目标图片的选择

import { UploadOutlined } from '@ant-design/icons';
import { Upload } from 'antd';
import { Button } from 'antd-mobile';

const [image, setImage] = useState(); // 记录图片,没有图片时toBlob会报错

const replaceImg = (img) => {
  setImage(undefined);
  // 通过FileReader读取用户选取的图片
  const reader = new FileReader();
  reader.readAsDataURL(img);
  //加载图片后获取到图片的base64格式
  reader.onload = ({ target: { result } = {} }) => {
    //更新替换为目标图片
    cropper.replace(result);
    setImage(img);
  };
  return false;
};

<Upload fileList={[]} beforeUpload={replaceImg} accept="image/*">
  <Button className={styles.upload} icon={<UploadOutlined />}>
    选择图片
  </Button>
</Upload>

 五、裁剪后图片的获取及上传

const [loading, setLoading] = useState(false); // 记录上传的状态

const onSubmit = () => {
  if (image) {
    setLoading(true);
    // 获取HTMLCanvasElement.toBlob获取blob,并通过FormData上传至服务器
    cropper
      .getCroppedCanvas({
        width: LONG,
        maxWidth: LONG, // maxWidth、maxHeight必须设置,原因见:遇到的bug和解决方案
        height: WIDE,
        maxHeight: WIDE, // maxWidth、maxHeight必须设置,原因见:遇到的bug和解决方案
      })
      .toBlob((blob) => {
        setLoading(false);
        if (blob) {
          console.log(blob);
          // const payload = new FormData();
          // payload.append('img', blob, '.png');
          // ...
        }
      }, 'image/png');
  }
};

<Button inline className={styles.button} type="primary" loading={loading} onClick={onSubmit}>
  确定上传
</Button>

源码:

import React, { useEffect, useRef, useState } from 'react';
import styles from './dome.scss';
import 'cropperjs/dist/cropper.css';
import CropperJs from 'cropperjs';
import { UploadOutlined } from '@ant-design/icons';
import { Upload } from 'antd';
import { Button } from 'antd-mobile';
const [LONG, WIDE] = [1512, 1039]; // 5寸照片尺寸
export default () => {
  const ref = useRef();
  const [cropper, setCropper] = useState(); // 存储cropper对象
  const [image, setImage] = useState(); // 记录图片,没有图片时toBlob会报错
  const [loading, setLoading] = useState(false); // 记录上传的状态

  useEffect(() => {
    const myCropper = new CropperJs(ref.current, {
      viewMode: 1,
      dragMode: 'move',
      aspectRatio: LONG / WIDE,
      autoCropArea: 0.9,
      highlight: false,
      cropBoxResizable: false,
      toggleDragModeOnDblclick: false,
    });
    setCropper(myCropper);
  }, []);

  const replaceImg = (img) => {
    setImage(undefined);
    // 通过FileReader读取用户选取的文件
    const reader = new FileReader();
    reader.readAsDataURL(img);
    //加载图片后获取到图片的base64格式
    reader.onload = ({ target: { result } = {} }) => {
      //更新替换为目标图片
      cropper.replace(result);
      setImage(img);
    };
    return false;
  };

  const onSubmit = () => {
    if (image) {
      setLoading(true);
      // 获取HTMLCanvasElement.toBlob获取blob,并通过FormData上传至服务器
      cropper
        .getCroppedCanvas({
          width: LONG,
          maxWidth: LONG, // maxWidth、maxHeight必须设置,原因见:遇到的bug和解决方案
          height: WIDE,
          maxHeight: WIDE, // maxWidth、maxHeight必须设置,原因见:遇到的bug和解决方案
        })
        .toBlob((blob) => {
          setLoading(false);
          if (blob) {
            console.log(blob);
            // const payload = new FormData();
            // payload.append('img', blob, '.png');
            // ...
          }
        }, 'image/png');
    }
  };

  return (
    <div className={styles.container}>
      <div className={styles.title}>{image?.name ?? '请上传图片'}</div>
      <div className={styles.cropper}>
        <img ref={ref} alt="" />
      </div>
      <Upload fileList={[]} beforeUpload={replaceImg} accept="image/*">
        <Button className={styles.upload} icon={<UploadOutlined />}>
          选择图片
        </Button>
      </Upload>
      <Button inline className={styles.button} type="primary" loading={loading} onClick={onSubmit}>
        确定上传
      </Button>
    </div>
  );
};

.container {
  height: 100%;
  padding: 0 24px;
  background-color: #fff;

  .title {
    height: 68px;
    line-height: 68px;
    font-size: 32px;
    font-weight: bold;
    text-align: center;
  }

  .cropper {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 60%;

    img {
      max-width: 100%;
      max-height: 100%;
    }
  }

  .upload {
    width: 240px;
    height: 68px;
    margin: 20px auto 0;
    font-size: 28px;
    border-radius: 8px;
    border: 2px solid #f0f0f0;
  }

  .button {
    display: block;
    width: 240px;
    height: 80px;
    line-height: 76px;
    margin: 80px auto 0;
    font-size: 32px;
    border: 2px solid #f0f0f0;
  }
}

最终效果: 

遇到的bug和解决方案:

    正式环境下,出现少量用户裁剪后上传的图片为白屏,宽高尺寸正常但大小极低。
    由于问题出现的概率很低,导致这个问题排查了很久,但却是一个实实在在存在的bug。
    假设用户误操作在空白区域裁剪,我们加上了裁剪区域及拖拽区域限制,确保了用户裁剪区域不会超过图片边缘。
    假设用户手机性能较低,我们排查了出现问题用户的订单,发现同一订单下其他图片能正常裁剪上传,且将源图上传到后台后发现部分白屏图片二次裁剪上传是能成功的。
    假设用户上传图片较大,我们自己尝试超大图片,发现超大图依然能上传,只是可能图片处理的速度及上传速度较慢。
    ...

沿着大图处理及上传速度较慢的思路,最终我们翻阅文档后发现这么一句话:

>Avoid get a blank (or black) output image, you might need to set the maxWidth and maxHeight properties to limited numbers, because of the size limits of a canvas element. Also, you should limit the maximum zoom ratio (in the zoom event) for the same reason.

>以及别人在stackoverflow上的解答:https://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element

最终,我们才确定了问题的原因:**不同浏览器会对HTML canvas 元素施加不同大小尺寸的限制,且这些限制的大小会随着平台和硬件而进行变化,超过限制后的画布将无法使用。离谱的是,即使你创建使用了超过限制大小的canvas,浏览器并没有提供任何类型的反馈,这就使得你无法知晓并去处理这类问题。**这就是导致HTMLCanvasElement.toBlob获取到空白图片却无任何报错的原因。
 回归到问题本身,我们只需要在getCroppedCanvas时设置不超出浏览器限制画布尺寸的maxWidth、maxHeight即可。
若要测试获取不同浏览器对canvas的限制,可考虑使用[canvas-size](https://github.com/jhildenbiddle/canvas-size)。

感谢大家的观看,若有疑问留言,大家可以一起沟通。😊
    
    
    
    
    
    

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值