基于 React 和 antd 实现的图片裁剪压缩功能

蚊子前端博客
发布于 2023-10-11 09:23
我们有很多上传图片的场景,都要用到裁剪和压缩的功能!

我们在日常开发过程中,有很多需要上传图片的场景。那用户在上传时,通常会遇到两个问题:

  1. 不知道前端显示的尺寸比例是多少,导致最终上传图片会变形;

  2. 图片质量或尺寸过大,比如可能就是个小 icon 图或者小封面图之类的,用户上传了一个好几兆的图片,尺寸可能对,但质量太大了,着实没有必要;

针对这种图片资源上传的类型,我们有必要在前端控制他的上传尺寸和上传质量。我这里讲下我们在业务中使用到的图片裁剪和压缩功能。

我们主要实现的功能:

  1. 读取 form 表单的内容,能回显图片;

  2. 能够按照裁剪比例进行裁剪;

  3. 可以指定最终生成图片的尺寸;

下面开始一一讲解下整个过程。

1. 上传图片

这里我们封装一个名叫ReactImageCropper的裁剪组件,同时接收从<Form.Item />传进来的参数:value, onChange 和 id(可选),主要是方便在 form 表单中使用。

选择上传图片的的功能,我直接使用了 antd 中的<Upload />组件:

COPYJAVASCRIPT

import { Button, Upload } from "antd"; const ReactImageCropper = ({ value, onChange, id }: any) => { const [originalUrl, setOriginalUrl] = useState(""); // 刚上传得到的原始图片地址 const handleUpload = (event: any) => { const { file } = event; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { // 将读取的图片资源转为base64,接下来进行裁剪的过程 setOriginalUrl(reader.result as string); }; reader.onerror = () => { message.error('读取文件失败'); }; }; return ( <div id={id}> <Upload accept="image/*" listType="picture-card" showUploadList={false} customRequest={handleUpload} > {value ? ( <img src={value} alt={id} style={{ maxWidth: "140px" }} /> ) : ( <Button>上传</Button> )} </Upload> </div> ); };

得到上传文件后,通过 FileReader 得到该文件的 base64 地址,方便我们后续的裁剪和压缩处理。

2. 裁剪

裁剪过程,我们是使用到了 react-cropper 的组件,将其放到 <Modal />组件中,裁剪完毕后,关闭弹窗。

这里我们主要是关注 2 个问题:

  1. 裁剪的比例是多少?

  2. 最终生成图片的尺寸是多少?

  3. 对质量大小有没有要求?

关于裁剪比例(即宽高比),一种是直接通过 aspectRatio 属性来设置比例。再有一种是通过设置的最终压缩尺寸,算出设置比例;比如我们最终需要的图片尺寸是 750*440,那比例就是 1.7 左右(750/440)。

若两者都存在,我们以最终输出的图片的尺寸算出来的比例优先,毕竟若两者不一致时,裁剪出来的图片和最终生成的图片,可能不一致。

我们上第 1 节获取到了上传图片的本地链接originalUrl,这里将其传入到 <Cropper /> 中。

COPYJAVASCRIPT

import Cropper from "react-cropper"; const ReactImageCropper = ({ width, height, aspectRatio }) => { const tempUrlRef = useRef(""); /** * 获取裁剪图片的宽高比 */ const aspect = useMemo(() => { if (width && height) { return width / height; } if (aspectRatio) { return aspectRatio; } return 1; }, [aspectRatio, width, height]); // 拖动结束时,获取裁剪后的图片,将其存储到临时变量等待进一步压缩处理 const onCrop = () => { const cropper: any = cropperRef?.current?.cropper; if (!cropper) { return; } const src = cropper.getCroppedCanvas().toDataURL(); tempUrlRef.current = src; }; // 点击ok的时候,表示已裁剪好,开始按照约定的尺寸压缩图片 // getImageFinalSize 和 compressImage ,在第3节会讲到 const handleComporess = async () => { const { width, height } = await getImageFinalSize(); // 获取最终图片的尺寸 console.log(width, height); // 对base64的图片进行压缩,然后得到压缩后的图片 const file = await compressImage({ info: { base64: tempUrlRef.current }, width, height, }); // 该上传该文件了 console.log("file", file); }; return ( <Modal open={Boolean(originalUrl)} onCancel={() => setOriginalUrl("")} destroyOnClose maskClosable={false} width={600} onOk={handleComporess} > <Cropper cropend={onCrop} ref={cropperRef} src={originalUrl} viewMode={1} aspectRatio={aspect} style={{ height: 400, width: "100%" }} guides={false} /> </Modal> ); };

这个回调onCrop()在每次裁剪结束时都会触发,得到一个新的裁剪后的图片地址,但只有点击弹窗中的确定按钮后,才会指定压缩的操作。

3. 压缩

压缩的过程稍微长点,主要经过以下的几个步骤:

  1. 获取最终要生成的图片的尺寸,若没有指定,则使用裁剪时得到的图片尺寸;

  2. 使用 canvas,将图片压缩至指定的尺寸;

  3. 把 base64 图片转成 File 对象,等待接口的上传;

下面来一一讲解。

3.1 获取要生成的图片的尺寸

若开发者指定了宽度和高度,最终的图片就是这个尺寸,我们就使用已指定好的;若不在乎最终尺寸,则依照裁剪图片时得到的尺寸。

COPYJAVASCRIPT

/** * 获取图片最终的宽高 * 若传入了宽高的数值,则直接使用;否则就使用裁剪出来的尺寸 */ const getImageFinalSize = () => { if (width && height) { return Promise.resolve({ width, height }); } const img = new Image(); return new Promise((resolve) => { img.onload = () => { resolve({ width: img.naturalWidth, height: img.naturalHeight }); }; // 读取刚才裁剪后的图片地址 img.src = tempUrlRef.current; }); };

获取到尺寸后,就会进入到下一步。

3.2 将图片压缩至指定尺寸

裁剪后的图片一般地只是比例符合要求,但宽高尺寸实际上可能还是很大。这里我们使用 canvas 对其进行压缩。

COPYJAVASCRIPT

/** * 将base64图片压缩到指定尺寸 */ const compressCurSize = ({ url, width, height, }: { url: string, // base64图片的地址 width: number, height: number, }): Promise<{ url: string, ext: string }> => { const [match, imageType] = url.match(/data:image\/(.*?);/) ?? []; if (!match || !imageType) { return Promise.reject( new TypeError(`imgurl should be base64, your enter: ${url}`) ); } const newImage = new Image(); newImage.src = url; return new Promise((resolve, reject) => { newImage.onload = function () { const canvas = document.createElement("canvas"); const ctx: any = canvas.getContext("2d"); canvas.width = width; canvas.height = height; // 注意这里的 fillStyle ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(this, 0, 0, canvas.width, canvas.height); canvas.toBlob( (blob) => { const file = new File( [blob as BlobPart], fileName || `${Date.now().toString(32)}.${imageType}` ); resolve({ file, ext: imageType }); }, `image/${imageType}`, 0.96 ); }; newImage.onerror = reject; }); };

属性 fillStyle 是用来填充背景颜色,若允许上传 png 格式的图片,请一定要注意这里。如果可以的话,就将其设置为白底(#fff),若想要保留透明,需要将其设置成透明色(rgba(255, 255, 255, 0))。若不设置 fillStyle,图片的透明底会被转为黑底。

渲染完毕后,可以通过 cavans 的 toBlob()方法,直接将 blob 转为 File 对象。

到这里,裁剪压缩的过程基本是已经完成了。接下来就是通过接口上传的步骤了,各自按照接口的要求上传即可。

4. 我不想使用 Upload 组件

有的同学可能不想使用 antd 中的<Upload />组件,主要也是考虑到这个组件的很多功能都用不上。那可以用 <input type="file"> 标签来实现。

COPYJAVASCRIPT

const ReactImageCropper = () => { const inputRef = useRef<HTMLInputElement | null>(null); const [originalUrl, setOriginalUrl] = useState(''); // 刚上传得到的原始图片地址 const handleChange = (event: any) => { const file = event.target.files[0]; inputRef.current.value = ''; const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { // 将读取的图片资源转为base64,接下来进行裁剪的过程 setOriginalUrl(reader.result as string); }; reader.onerror = () => { message.error('读取文件失败'); }; }; return ( <div> <input type="file" ref={inputRef} onChange={handleChange} /> </div> ); };

回调函数 handleChange() 中,有一个将 value 清空的步骤。这主要是为了避免上传同一份图片时,该 change 方法不会触发。因为该回调函数触发的条件得是 value 发生了变动。

接下来的裁剪、压缩等步骤跟上面的一样。

5. 总结

到这里,图片整个的裁剪压缩过程已完成了。我们来模拟下业务里的场景,比如用户上传的头像图片,最终尺寸为 120*120 差不多就够用了。看下实现的效果:

裁剪压缩后的图片-蚊子的前端博客

有的同学可能也想把它封装成组件,然后把<Upload />或者其他组件以子组件的方式穿进去。后续我们会讲解如何封装该组件。

标签:
阅读(139)

公众号:

qrcode

微信公众号:前端小茶馆