一.首先安装react-cropper插件
npm install --save react-cropper
执行该命令以后,下载react-cropper依赖信息自动更新到package.json中
在使用该插件的代码中需要进行引入
import "cropperjs/dist/cropper.css"
import Cropper from 'react-cropper'
二.如下列出相应插件的文档以供参考
react-cropper文档:https://www.npmjs.com/package/react-cropperjs
官方cropper文档:https://fengyuanchen.github.io/cropper/
一个作者总结的相关插件的中文版文档:https://blog.csdn.net/weixin_38023551/article/details/78792400
三.主要的实现的功能
使用antdesign的upload组件进行上传图片,并调用react-cropper组件实现固定长和宽的截图,将截图的结果以FILE格式传给后台。实现效果如下。
可以上传多张图片。
四.主要的思路
首先构建一个CropperUpload组件,该组件接收截图框的长,宽,Upload样式模式,初始图片列表四个参数。该组件主要实现上传可截图的图片的功能,在Upload组件的beforeUpload中用FileReader读取要上传的文件的base64码,并返回false阻止Upload组件自动上传,将文件码赋值给cropper组件,用cropper进行截图,截图之后,将cropper返回的base64码转换成File格式传向后台。
五.具体函数的实现
1.首先需要一个ant的Upload组件,在这里主要用到这个组件的三个重要参数,beforeUpload上传图片之前触发的回调,onRemove删除图片的回调,fileList已上传图片的列表。
<Upload
name="files"
listType={
this.state.pattern === 1 ? " " : this.state.pattern === 2 ? "picture-card" : "picture"}
className={
this.state.pattern === 3 ? styles.uploadinline : ""}
beforeUpload={
this.beforeUpload.bind(this)}
onRemove={
this.handleRemove.bind(this)}
fileList={
this.state.fileList}
>
{
botton}
</Upload>
其他的listType主要是支持Upload不同的样式模式。如图所示的三种模式。
点击上传图片按钮会触发beforeUpload函数,如果想获取通过input选择的文件的数据,我们就需要使用到js封装好的FileReader对象。并通过input.files[0]来获取点击上传的文件对象,在这里因为用了Upload组件,该组件已经封装好了,此时会通过beforeUpload的file参数传递进来,文件对象里面包括文件大小(size),文件的名字(name)和文件类型(type),可以通过该文件类型来判断上传的文件是否是图片,判断的正则如下
/image\/\w+/.test(file.type)
通过调用FileReader来读取文件,文件读取结束之后会调用onload回调,在onload回调中,在通过image.onload回调(该回调是异步的)来获取图片的长宽,图片的长宽必须大于裁剪框的长宽,最后通过e.target.result来获取图片的base64码,将该码通过setSate设置给reactCropper的src中,src为裁剪插件的图片的源,在将包裹裁剪框的Modal框弹出,通过将Modal的visible设置为true,Model的visible在这里用state中的editImageModalVisible来控制,此时将弹出裁剪图片的裁剪框。
// 特产介绍图片Upload上传之前函数
beforeUpload(file, fileList) {
//当打开同一张图片的时候清除上一次的缓存
if (this.refs.cropper) {
this.refs.cropper.reset();
this.refs.cropper.setData({
width: this.state.width,
height: this.state.height,
});
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
//添加文件限制
MsgBox.error({
content: '文件大小不能超过10M' });
return false;
}
var reader = new FileReader();
const image = new Image();
var height;
var width;
//因为读取文件需要时间,所以要在回调函数中使用读取的结果
reader.readAsDataURL(file); //开始读取文件
reader.onload = (e) => {
image.src = reader.result;
image.onload = () => {
height = image.naturalHeight;
width = image.naturalWidth;
if (height < this.state.height || width < this.state.width) {
message.error('图片尺寸不对 宽应大于:'+this.state.width+ '高应大于:' +this.state.height );
this.setState({
editImageModalVisible: false, //打开控制裁剪弹窗的变量,为true即弹窗
})
}
else{
this.setState({
srcCropper: e.target.result, //cropper的图片路径
selectImgName: file.name, //文件名称
selectImgSize: (file.size / 1024 / 1024), //文件大小
selectImgSuffix: file.type.split("/")[1], //文件类型
editImageModalVisible: true, //打开控制裁剪弹窗的变量,为true即弹窗
})
if (this.refs.cropper) {
this.refs.cropper.replace(e.target.result);
}
}
}
}
return false;
}
裁剪框的设置如下,src是裁剪框图片的路径,ref为该组件真实实例的引用,viewMode在这里选择了模式1,zoomable是否允许放大图像,movable是否允许移动图像,在这里设置了ready函数,当一个cropper实例完全构建时,这个事件就会发生,用于在每一次裁剪之前都重新设置一下裁剪框大小,否则他会自动的恢复默认原图像0.8比例。
<Cropper
src={
this.state.srcCropper} //图片路径,即是base64的值,在Upload上传的时候获取到的
ref="cropper"
viewMode={
1} //定义cropper的视图模式
zoomable={
true} //是否允许放大图像
movable={
true}
guides={
true} //显示在裁剪框上方的虚线
background={
false} //是否显示背景的马赛克
rotatable={
false} //是否旋转
style={
{
height: '100%', width: '100%' }}
cropBoxResizable={
false}//是否可以拖拽
cropBoxMovable={
true}//是否可以移动裁剪框
dragMode="move"
center={
true}
ready={
this._ready.bind(this)}
/>
裁剪框父组件设置如下,可见与不可见通过页面的stated的editImageModalVisible来控制,当上传图片时此状态设置为true显示裁剪框,当上传图片成功,或者关闭裁剪框的时候将此状态设置为false关闭裁剪框,Modal组件的高度是自适应组件内的图片高度的,宽度并不自适应图片的宽度,若不设置将会有默认的宽度,因此在这里设置为100%,当裁剪完成点击保存触发saveImg函数回调,当点击取消的时候触发handleCancle回调。
<Modal
key="cropper_img_icon_key"
visible={
this.state.editImageModalVisible}
width="600"
footer={
[
<Button type="primary" onClick={
this.saveImg.bind(this)} >保存</Button>,
<Button onClick={
this.handleCancel.bind(this)} >取消</Button>
]}>
</Modal>
点击保存按钮之后,触发saveImg函数,要使用FormData向后台传数据,并且传输的是格式是FILE格式的,因此现将截图的结果取出,取出的时候使用this.refs.cropper.getCroppedCanvas().toDataURL(),最终取出的是截图后图片的base64码,通过dataURLtoFile函数将base64转换成FILE,封装到formdata中,调用后台的一个接口postimage。后台将返回一个图片的路径给前台。前台将路径取出,并按照Upload组件图片列表默认显示的格式来构建,最终push到fileList中,显示在Upload组件上。并将state中editImageModalVisible的状态置为false,关闭Modal裁剪框。
saveImg() {
const {
dispatch } = this.props;
var formdata = new FormData();
formdata.append("files", this.dataURLtoFile(this.refs.cropper.getCroppedCanvas().toDataURL(), this.state.selectImgName));
dispatch({
type: 'getData/postimage',
payload: formdata,
callback: () => {
const {
success, msg, obj } = this.props.imagePictures;
if (success) {
let imageArry = this.state.pattern == 3 ? this.state.fileList.slice() : [];
imageArry.push({
uid: Math.random() * 100000,
name: this.state.selectImgName,
status: 'done',
url: obj[0].sourcePath,
// url:obj[0].sourcePath,
thumbUrl: this.refs.cropper.getCroppedCanvas().toDataURL(),
thumbnailPath: obj[0].thumbnailPath,
largePath: obj[0].largePath,
mediumPath: obj[0].mediumPath,
upload: true,
})
this.setState({
fileList: imageArry,
editImageModalVisible: false, //打开控制裁剪弹窗的变量,为true即弹窗
srcCropper: this.state.srcCropper, //cropper的图片路径
})
this.props.onChange(imageArry);
}
else {
message.error(msg);
}
},
});
}
在modal中调用getData的postimage向后台发送数据,现在附上请求头的设置部分,因为传递的数据已经用formdata封装上了,因此直接就传递给后台就可以。
export default function request(url, options) {
const defaultOptions = {
credentials: 'include',
};
const newOptions = {
...defaultOptions, ...options };
if (newOptions.method === 'POST' || newOptions.method === 'PUT') {
newOptions.method = 'POST'
}
return fetch(url, newOptions)
.then(checkStatus)
.then((response) => {
if (newOptions.method === 'DELETE' || response.status === 204) {
return response.text();
}
return response.json();
})
.catch((e) => {
const {
dispatch } = store;
const status = e.name;
if (status === 401) {
dispatch(routerRedux.push('/user/login'));
return;
}
});
}
向后台发送图片的请求头:
后传传输回数据:图片存储的路径,分为不同种的格式,大图,小图,中图,原图。
六遇到的问题
上面流程看似简单,可是在实际开发中遇到了无数的困难!下面给大家分享我的经历。
1.beforeUpload组件问题
当Upload组件调用beforeUpload函数的时候,如果函数的返回值是false的时候,就会清空fileList,所以每次参数fileList之中都只有当前的图片,没有之前上传过的,这是因为当beforeUpload函数返回时false的时候,会自动触发onChange函数,触发时fileList中只有当前的上传的图片,因此若再用onChange函数中调用setState来维护fileList就会将之前的图片都清除。因此在这种完全阻止自动上传的情景下就应该将onChange函回调函数删除。
<Upload
name="files"
action="/hyapi/resource/image/multisize/upload"
beforeUpload={
this.beforeUpload.bind(this)}
onRemove={
this.handleRemove.bind(this)}
fileList={
this.state.fileList}
//onChange={this.onChange}//这里将不使用onChange回调
>
{
botton}
</Upload>
2.对同一张图片进行截图的问题
当对一张图片进行截图,调整了图片的位置和缩放,下次再打开同一张图片,还会保留有上一次的记录。
第一次打开的位置:
改变图片大小和位置:
再次打开时候还是上图所示位置,cropper对同一张图片没有重新的渲染。cropper组件有一个ready函数,每一次截图准备完毕之前都会调用,在ready里面打印,发现同一张图片连续上传,只有第一次会调用ready。
_ready() {
this.refs.cropper.setData({
width: this.state.width,
height: this.state.height,
});
}
这说明对同一张图片,cropper不会重新渲染,所以就要手动在每次弹出截图框的时候进行reset一下,因此在beforeUpload函数中进行恢复初始化操作。调用this.refs.cropper.reset(),并重新设置裁剪框的宽和高,否则会恢复默认原图的0.8比例。
beforeUpload(file, fileList) {
//当打开同一张图片的时候清除上一次的缓存
if (this.refs.cropper) {
this.refs.cropper.reset();
this.refs.cropper.setData({
width: this.state.width,
height: this.state.height,
});
}
3.多次截图后面图片会变小,容器变成默认的200*100
第一次上传图片截图时正常,图片大小为原始图片大小
后来在截图发现图片会压缩成很小,查看样式发现为cropper-container恢复默认值200*100
发现单纯的通过setState改变cropper的src有时并不能使图片将cropper容器撑开,达到重新渲染组件的目的,后来查看了官方文档,发现有一个参数replace(url),作用替换图像的src并重新构建cropper,尝试了一下发现可以重新构建cropper,达到图片撑开容器而不是显示容器默认值得效果。并且加上这个参数之后。连续上传同一张图片也不会显示上一次默认的值了,同一张图片也会当做一张新的图片重新渲染,所以每次给cropper赋值的时候,调用replace函数十分重要,而不是单纯的靠改变cropper的src来实现重新渲染裁剪插件。
this.setState({
srcCropper: e.target.result, //cropper的图片路径
selectImgName: file.name, //文件名称
selectImgSize: (file.size / 1024 / 1024), //文件大小
selectImgSuffix: file.type.split("/")[1], //文件类型
editImageModalVisible: true, //打开控制裁剪弹窗的变量,为true即弹窗
})
if (this.refs.cropper) {
this.refs.cropper.replace(e.target.result);
}
4.用fetch向后台传输File格式文件,头文件设置问题
将File绑定在formdata里面之后,向后台传递时候,起初手动设置了头文件Content-Type=multipart/form-data,向后台发送数据失败,头文件显示如下,显示的结果没有正常情况下的boundary
看了原生ajax传递数据的代码如下,将contentType设置为false,不设置头文件。因此尝试在fetch时候,不设置contentType的内容。
$.ajax({
url: "upload.ashx",
type: "POST",
data: formData,
/**
*必须false才会自动加上正确的Content-Type
*/
contentType: false,
/**
* 必须false才会避开jQuery对 formdata 的默认处理
* XMLHttpRequest会对 formdata 进行正确的处理
*/
processData: false,
success: function (data) {
if (data.status == "true") {
alert("上传成功!");
}
if (data.status == "error") {
alert(data.msg);
}
$("#imgWait").hide();
},
error: function () {
alert("上传失败!");
$("#imgWait").hide();
}
})
之后再向后台传递的时候,头文件就显示为正常,自动设置了boundary
以上是所有开发中遇到的问题,欢迎大家一起来讨论
七.完整代码部分
自己封装的cropper-upload组件完整代码
import React, {
PureComponent } from 'react';
import moment from 'moment';
import {
routerRedux, Route, Switch, Link } from 'dva/router';
import {
Upload, Button, Modal, Icon, message } from 'antd';
import "cropperjs/dist/cropper.css"
import Cropper from 'react-cropper'
import {
connect } from 'dva';
import styles from './Upload.less';
@connect(state => ({
imagePictures: state.getData.imagePictures,
}))
class CropperUpload extends PureComponent {
constructor(props) {
super(props);
console.log(props);
console.log("props");
this.state = {
width: props.width,
height: props.height,
pattern: props.pattern,
fileList: props.fileList ? props.fileList : [],
editImageModalVisible: false,
srcCropper: '',
selectImgName: '',
}
}
componentWillReceiveProps(nextProps) {
if ('fileList' in nextProps) {
this.setState({
fileList: nextProps.fileList ? nextProps.fileList : [],
});
}
}
handleCancel = () => {
this.setState({
editImageModalVisible: false,
});
}
// 图片Upload上传之前函数
beforeUpload(file, fileList) {
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
//添加文件限制
MsgBox.error({
content: '文件大小不能超过10M' });
return false;
}
var reader = new FileReader();
const image = new Image();
var height;
var width;
//因为读取文件需要时间,所以要在回调函数中使用读取的结果
reader.readAsDataURL(file); //开始读取文件
reader.onload = (e) => {
image.src = reader.result;
image.onload = () => {
height = image.naturalHeight;
width = image.naturalWidth;
if (height < this.state.height || width < this.state.width) {
message.error('图片尺寸不对 宽应大于:'+this.state.width+ '高应大于:' +this.state.height );
this.setState({
editImageModalVisible: false, //打开控制裁剪弹窗的变量,为true即弹窗
})
}
else{
this.setState({
srcCropper: e.target.result, //cropper的图片路径
selectImgName: file.name, //文件名称
selectImgSize: (file.size / 1024 / 1024), //文件大小
selectImgSuffix: file.type.split("/")[1], //文件类型
editImageModalVisible: true, //打开控制裁剪弹窗的变量,为true即弹窗
})
if (this.refs.cropper) {
this.refs.cropper.replace(e.target.result);
}
}
}
}
return false;
}
handleRemove(file) {
this.setState((state) => {
const index = state.fileList.indexOf(file);
const newFileList = state.fileList.slice();
newFileList.splice(index, 1);
this.props.onChange(newFileList);
return {
fileList: newFileList,
};
});
}
//将base64码转化成blob格式
convertBase64UrlToBlob(base64Data) {
var byteString;
if (base64Data.split(',')[0].indexOf('base64') >= 0) {
byteString = atob(base64Data.split(',')[1]);
} else {
byteString = unescape(base64Data.split(',')[1]);
}
var mimeString = base64Data.split(',')[0].split(':')[1].split(';')[0];
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {
type: this.state.selectImgSuffix });
}
//将base64码转化为FILE格式
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {
type: mime });
}
_ready() {
this.refs.cropper.setData({
width: this.state.width,
height: this.state.height,
});
}
saveImg() {
const {
dispatch } = this.props;
var formdata = new FormData();
formdata.append("files", this.dataURLtoFile(this.refs.cropper.getCroppedCanvas().toDataURL(), this.state.selectImgName));
dispatch({
type: 'getData/postimage',
payload: formdata,
callback: () => {
const {
success, msg, obj } = this.props.imagePictures;
if (success) {
let imageArry = this.state.pattern == 3 ? this.state.fileList.slice() : [];
imageArry.push({
uid: Math.random() * 100000,
name: this.state.selectImgName,
status: 'done',
url: obj[0].sourcePath,
// url:obj[0].sourcePath,
thumbUrl: this.refs.cropper.getCroppedCanvas().toDataURL(),
thumbnailPath: obj[0].thumbnailPath,
largePath: obj[0].largePath,
mediumPath: obj[0].mediumPath,
upload: true,
})
this.setState({
fileList: imageArry,
editImageModalVisible: false, //打开控制裁剪弹窗的变量,为true即弹窗
})
this.props.onChange(imageArry);
}
else {
message.error(msg);
}
},
});
}
render() {
const botton = this.state.pattern == 2 ?
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div> :
<Button>
<Icon type="upload" />选择上传</Button>
return (
<div>
<Upload
name="files"
action="/hyapi/resource/image/multisize/upload"
listType={
this.state.pattern === 1 ? " " : this.state.pattern === 2 ? "picture-card" : "picture"}
className={
this.state.pattern === 3 ? styles.uploadinline : ""}
beforeUpload={
this.beforeUpload.bind(this)}
onRemove={
this.handleRemove.bind(this)}
fileList={
this.state.fileList}
>
{
botton}
</Upload>
<Modal
key="cropper_img_icon_key"
visible={
this.state.editImageModalVisible}
width="100%"
footer={
[
<Button type="primary" onClick={
this.saveImg.bind(this)} >保存</Button>,
<Button onClick={
this.handleCancel.bind(this)} >取消</Button>
]}>
<Cropper
src={
this.state.srcCropper} //图片路径,即是base64的值,在Upload上传的时候获取到的
ref="cropper"
preview=".uploadCrop"
viewMode={
1} //定义cropper的视图模式
zoomable={
true} //是否允许放大图像
movable={
true}
guides={
true} //显示在裁剪框上方的虚线
background={
false} //是否显示背景的马赛克
rotatable={
false} //是否旋转
style={
{
height: '100%', width: '100%' }}
cropBoxResizable={
false}
cropBoxMovable={
true}
dragMode="move"
center={
true}
ready={
this._ready.bind(this)}
/>
</Modal>
</div>
);
}
}
export default CropperUpload;
页面具体调用范例:
<CropperUpload
pattern={
1}//模式分为三种,1为只能上传一张图片,2为上传多张图片
width={
375}
height={
120}
onChange={
this.ChangeIconImage.bind(this)}
fileList={
this.state.iconUrl}
/>