Cropper.js + React 實(shí)現(xiàn)前端圖片裁剪上傳

前言:記錄一次使用cropper.js+react實(shí)現(xiàn)圖片裁剪上傳的過(guò)程、源碼、遇到的bug和解決方案。

github地址:https://github.com/fengyuanchen/cropperjs
官網(wǎng)地址:https://fengyuanchen.github.io/cropperjs/
官網(wǎng)示例地址:https://fengyuanchen.github.io/photo-editor/

開發(fā)過(guò)程:

一、cropper.js的引入

import 'cropperjs/dist/cropper.css';
import CropperJs from 'cropperjs';
注:css及js均需引入

二、創(chuàng)建img元素并設(shè)置樣式

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將作為用于裁剪的目標(biāo)圖像或畫布元素,你可以預(yù)設(shè)src作為目標(biāo)圖像,或者也可以通過(guò)replace(url[, hasSameSize])去替換更新目標(biāo)圖像。
另外,cropper.js將掛載在img的父級(jí)元素下。所以你需要將img放置在塊級(jí)元素下,并設(shè)置img的最大寬高。

三、初始化實(shí)例并根據(jù)文檔及需求配置你的options

const [cropper, setCropper] = useState(); // 存儲(chǔ)cropper對(duì)象
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用于目標(biāo)圖片的選擇

import { UploadOutlined } from '@ant-design/icons';
import { Upload } from 'antd';
import { Button } from 'antd-mobile';
const [image, setImage] = useState(); // 記錄圖片,沒有圖片時(shí)toBlob會(huì)報(bào)錯(cuò)
const replaceImg = (img) => {
  setImage(undefined);
  // 通過(guò)FileReader讀取用戶選取的圖片
  const reader = new FileReader();
  reader.readAsDataURL(img);
  //加載圖片后獲取到圖片的base64格式
  reader.onload = ({ target: { result } = {} }) => {
    //更新替換為目標(biāo)圖片
    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); // 記錄上傳的狀態(tài)
const onSubmit = () => {
  if (image) {
    setLoading(true);
    // 獲取HTMLCanvasElement.toBlob獲取blob,并通過(guò)FormData上傳至服務(wù)器
    cropper
      .getCroppedCanvas({
        width: LONG,
        maxWidth: LONG, // maxWidth、maxHeight必須設(shè)置,原因見:遇到的bug和解決方案
        height: WIDE,
        maxHeight: WIDE, // maxWidth、maxHeight必須設(shè)置,原因見:遇到的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(); // 存儲(chǔ)cropper對(duì)象
  const [image, setImage] = useState(); // 記錄圖片,沒有圖片時(shí)toBlob會(huì)報(bào)錯(cuò)
  const [loading, setLoading] = useState(false); // 記錄上傳的狀態(tài)

  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);
    // 通過(guò)FileReader讀取用戶選取的文件
    const reader = new FileReader();
    reader.readAsDataURL(img);
    //加載圖片后獲取到圖片的base64格式
    reader.onload = ({ target: { result } = {} }) => {
      //更新替換為目標(biāo)圖片
      cropper.replace(result);
      setImage(img);
    };
    return false;
  };

  const onSubmit = () => {
    if (image) {
      setLoading(true);
      // 獲取HTMLCanvasElement.toBlob獲取blob,并通過(guò)FormData上傳至服務(wù)器
      cropper
        .getCroppedCanvas({
          width: LONG,
          maxWidth: LONG, // maxWidth、maxHeight必須設(shè)置,原因見:遇到的bug和解決方案
          height: WIDE,
          maxHeight: WIDE, // maxWidth、maxHeight必須設(shè)置,原因見:遇到的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 ?? '請(qǐng)上傳圖片'}</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;
  }
}

最終效果:

WechatIMG679.png

遇到的bug和解決方案:

正式環(huán)境下,出現(xiàn)少量用戶裁剪后上傳的圖片為白屏,寬高尺寸正常但大小極低。

由于問(wèn)題出現(xiàn)的概率很低,導(dǎo)致這個(gè)問(wèn)題排查了很久,但卻是一個(gè)實(shí)實(shí)在在存在的bug。
假設(shè)用戶誤操作在空白區(qū)域裁剪,我們加上了裁剪區(qū)域及拖拽區(qū)域限制,確保了用戶裁剪區(qū)域不會(huì)超過(guò)圖片邊緣。
假設(shè)用戶手機(jī)性能較低,我們排查了出現(xiàn)問(wèn)題用戶的訂單,發(fā)現(xiàn)同一訂單下其他圖片能正常裁剪上傳,且將源圖上傳到后臺(tái)后發(fā)現(xiàn)部分白屏圖片二次裁剪上傳是能成功的。
假設(shè)用戶上傳圖片較大,我們自己嘗試超大圖片,發(fā)現(xiàn)超大圖依然能上傳,只是可能圖片處理的速度及上傳速度較慢。
...

沿著大圖處理及上傳速度較慢的思路,最終我們翻閱文檔后發(fā)現(xiàn)這么一句話:

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

最終,我們才確定了問(wèn)題的原因:不同瀏覽器會(huì)對(duì)HTML canvas 元素施加不同大小尺寸的限制,且這些限制的大小會(huì)隨著平臺(tái)和硬件而進(jìn)行變化,超過(guò)限制后的畫布將無(wú)法使用。離譜的是,即使你創(chuàng)建使用了超過(guò)限制大小的canvas,瀏覽器并沒有提供任何類型的反饋,這就使得你無(wú)法知曉并去處理這類問(wèn)題。這就是導(dǎo)致HTMLCanvasElement.toBlob獲取到空白圖片卻無(wú)任何報(bào)錯(cuò)的原因。
回歸到問(wèn)題本身,我們只需要在getCroppedCanvas時(shí)設(shè)置不超出瀏覽器限制畫布尺寸的maxWidth、maxHeight即可。
若要測(cè)試獲取不同瀏覽器對(duì)canvas的限制,可考慮使用canvas-size

感謝大家的觀看,若有疑問(wèn)留言,大家可以一起溝通。??
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容