背景
前段时间接了个需求,需要做个图片上传,预览及裁切服务,调研了下社区的开源方案,cropperjs 进入视野,简单看了下 README,完美符合我的需求,最终实现效果如下:

安装
没啥好说的,直接 yarn 走起:
1 | yarn add cropperjs |
引入 CSS 文件:
1 | import "cropperjs/dist/cropper.css"; |
封装
简单的分析下需求,整个图片裁切包含以下几个部件:
- 图片裁切框 - 包含「网页端」和「移动端」,需要分开裁切
- 替换图片按钮 - 替换图片后,「网页端」和「移动端」都需要替换
- 图片预览 - 同样「网页端」和「移动端」也需要分开预览
Cropper.js 是个原生的裁切库,在我们的 React 项目中按照上面的功能划分封装成以下几个组件:
- Upload.tsx
1 | import React, { useContext, memo } from "react"; |
这个组件的功能很简单,就是使用组件库的上传组件获取文件的 base64,然后保存到 context 里,方便裁切组件消费。
- Context.tsx
1 | import React, { createContext, FC, useMemo, useState, useRef } from "react"; |
- Prview.tsx
1 | import React, { useContext, memo } from "react"; |
预览组件本身也不复杂,就是需要注意的是,Cropper.js 的 preview 参数可以是个元素,元素数组或者能够被 Document.querySelectorAll 选中的 class 选择器,因为封装成了通用组件,因此使用了 lodash 的 uniqueId 方法生成唯一的 class,防止页面出现多个 Prview 组件时,class 类名冲突。
- Cropper.tsx
1 | import React, { useContext, memo, useRef, useEffect } from "react"; |
最后就是重头戏的 Cropper 了,组件内部封装了 Cropper.js 初始化逻辑,图片切换的逻辑。
- index.tsx
1 | import CropperInner from "./Cropper"; |
最后就是将这些组件全部挂载到 Cropper,对外只暴露 Cropper 这一个组件。
使用
使用封装后的 Cropper 组件大概如下,其中 Cropper 组件和 Cropper.Preview 组件可以有任意多个,但是需要注意,两个组件的 name 字段要一一对应,否则会出现无法实时预览的情况。
1 | import Cropper from "./Cropper"; |
样式覆盖
组件逻辑解决了,现在解决样式问题。我们之前引入了 cropperjs 的默认样式文件,现在只需要覆盖掉它的样式就可以了
1 | // 复写 Cropper 的样式 |
问题
- 获取裁切后图片 blob
图片裁切,需要获取裁切图片的 blob 上传到 CDN,Cropperjs 提供了获取 blob 的方法:
1 | cropper.getCroppedCanvas().toBlob((blob) => {}) |
但是在我们业务的场景下,同时会存在两个裁切框分别用来裁切「网页端」和「移动端」,用的都是同一个图片,也就是说替换图片会同时替换「网页端」和「移动端」的图片。这就导致了替换图片后,只有当前激活的 tab 下的 cropper 调用 getCroppedCanvas 才能获取到替换图片后的数据,没有激活的 tab 下还是获取旧图片的数据。
复现路径:

除非在提交前,手动切换到移动端的 tab,不然该问题会一直存在,这是 cropperjs 内部实现的问题,我们不能要求用户多这么一个无意义的步骤,因此我们自己实现 「getCroppedCanvas」方法,不用 cropperjs 暴露的方法
1 | // 使用 canvas 裁切图片 |