前言:記錄一次使用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;
}
}
最終效果:

遇到的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。