react-cropper + antdesign +dva 实现裁剪图片并上传的功能

react-cropper + antdesign +dva 实现裁剪图片并上传的功能

一.首先安装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}
   />
发布了38 篇原创文章 · 获赞 6 · 访问量 8544

猜你喜欢

转载自blog.csdn.net/qq_36400206/article/details/88672171
今日推荐