zoukankan      html  css  js  c++  java
  • 前端AntD框架的upload组件上传图片时遇到的一些坑

    前言

    本文的最新内容,将在GitHub上更新,欢迎 star,和我一起入门和进阶前端。

    本次做后台管理系统,采用的是 AntD 框架。涉及到图片的上传,用的是AntD的 upload 组件。

    前端做文件上传这个功能,是很有技术难度的。既然框架给我们提供好了,那就直接用呗。结果用的时候,发现 upload 组件的很多bug。下面来列举几个。

    备注:本文写于2019-03-02,使用的 antd 版本是 3.13.6。

    使用 AntD 的 upload 组件做图片的上传

    因为需要上传多张图片,所以采用的是照片墙的形式。上传成功后的界面如下:

    (1)上传中:

    (2)上传成功:

    (3)图片预览:

    按照官方提供的实例,特此整理出项目开发中的完整写法,亲测有效。代码如下:

    /* eslint-disable */
    
    import { Upload, Icon, Modal, Form } from 'antd';
    
    const FormItem = Form.Item;
    
    class PicturesWall extends PureComponent {
      state = {
        previewVisible: false,
        previewImage: '',
        imgList: [],
      };
    
    
      handleChange = ({ file, fileList }) => {
        console.log(JSON.stringify(file)); // file 是当前正在上传的 单个 img
        console.log(JSON.stringify(fileList)); // fileList 是已上传的全部 img 列表
    
        this.setState({
          imgList: fileList,
        });
      };
    
    
      handleCancel = () => this.setState({ previewVisible: false });
    
      handlePreview = file => {
        this.setState({
          previewImage: file.url || file.thumbUrl,
          previewVisible: true,
        });
      };
    
    
      // 参考链接:https://www.jianshu.com/p/f356f050b3c9
      handleBeforeUpload = file => {
        //限制图片 格式、size、分辨率
        const isJPG = file.type === 'image/jpeg';
        const isJPEG = file.type === 'image/jpeg';
        const isGIF = file.type === 'image/gif';
        const isPNG = file.type === 'image/png';
        if (!(isJPG || isJPEG || isGIF || isPNG)) {
          Modal.error({
            title: '只能上传JPG 、JPEG 、GIF、 PNG格式的图片~',
          });
          return;
        }
        const isLt2M = file.size / 1024 / 1024 < 2;
        if (!isLt2M) {
          Modal.error({
            title: '超过2M限制,不允许上传~',
          });
          return;
        }
        return (isJPG || isJPEG || isGIF || isPNG) && isLt2M && this.checkImageWH(file);
      };
    
      //返回一个 promise:检测通过则返回resolve;失败则返回reject,并阻止图片上传
      checkImageWH(file) {
        let self = this;
        return new Promise(function(resolve, reject) {
          let filereader = new FileReader();
          filereader.onload = e => {
            let src = e.target.result;
            const image = new Image();
            image.onload = function() {
              // 获取图片的宽高,并存放到file对象中
              console.log('file width :' + this.width);
              console.log('file height :' + this.height);
              file.width = this.width;
              file.height = this.height;
              resolve();
            };
            image.onerror = reject;
            image.src = src;
          };
          filereader.readAsDataURL(file);
        });
      }
    
      handleSubmit = e => {
        const { dispatch, form } = this.props;
        e.preventDefault();
        form.validateFieldsAndScroll((err, values) => {// values 是form表单里的参数
          // 点击按钮后,将表单提交给后台
          dispatch({
            type: 'mymodel/submitFormData',
            payload: values,
          });
        });
      };
    
      render() {
        const { previewVisible, previewImage, imgList } = this.state; //  从 state 中拿数据
        const uploadButton = (
          <div>
            <Icon type="plus" />
            <div className="ant-upload-text">Upload</div>
          </div>
        );
        return (
          <div className="clearfix">
            <Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 8 }}>
              <FormItem label="图片图片" {...formItemLayout}>
                {getFieldDecorator('myImg')(
                  <Upload
                    action="//jsonplaceholder.typicode.com/posts/" // 这个是接口请求,实际开发中,要替换成你自己的业务接口
                    data={file => ({ // data里存放的是接口的请求参数
                      param1: myParam1,
                      param2: myParam2,
                      photoCotent: file, // file 是当前正在上传的图片
                      photoWidth: file.height, // 通过  handleBeforeUpload 获取 图片的宽高
                      photoHeight: file.width,
                    })}
                    listType="picture-card"
                    fileList={this.state.imgList}
                    onPreview={this.handlePreview} // 点击图片缩略图,进行预览
                    beforeUpload={this.handleBeforeUpload} // 上传之前,对图片的格式做校验,并获取图片的宽高
                    onChange={this.handleChange} // 每次上传图片时,都会触发这个方法
                  >
                    {this.state.imgList.length >= 9 ? null : uploadButton}
                  </Upload>
                )}
              </FormItem>
            </Form>
            <Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
              <img alt="example" style={{  '100%' }} src={previewImage} />
            </Modal>
          </div>
        );
      }
    }
    
    export default PicturesWall;
    
    

    上传后,点击图片预览,浏览器卡死的问题

    依据上方的代码,通过 Antd 的 upload 组件将图片上传成功后,点击图片的缩略图,理应可以在当前页面弹出 Modal,预览图片。但实际的结果是,浏览器一定会卡死。

    定位问题发现,原因竟然是:图片上传成功后, upload 会将其转为 base64编码。base64这个字符串太大了,点击图片预览的时候,浏览器在解析一大串字符串,然后就卡死了。详细过程描述如下。

    上方代码中,我们可以把 handleChange(file, fileList)方法中的 file、以及 fileList打印出来看看。 file指的是当前正在上传的 单个 img,fileList是已上传的全部 img 列表。 当我上传完 两张图片后, 打印结果如下:

    file的打印的结果如下:

        {
            "uid": "rc-upload-1551084269812-5",
            "width": 600,
            "height": 354,
            "lastModified": 1546701318000,
            "lastModifiedDate": "2019-01-05T15:15:18.000Z",
            "name": "e30e7b9680634b2c888c8bb513cc595d.jpg",
            "size": 31731,
            "type": "image/jpeg",
            "percent": 100,
            "originFileObj": {
                "uid": "rc-upload-1551084269812-5",
                "width": 600,
                "height": 354
            },
            "status": "done",
            "thumbUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAHQ9qKKlbimcXrIH9o2vH/AC2T+ddPj98v+9RRWsuhnHdk0ar9qb5R0Pb6VPB/qh9aKKiRr0Irnt/vUDr+NFFJCRqWxJik5Pb+dLJ938aKK06mYSdKKKKBH//Z",
            "response": {
                "retCode": 0,
                "imgUrl": "http://qianguyihao.com/opfewfwj098902kpkpkkj976fe.jpg",
                "photoid": 271850
            }
        }
    

    fileList 的打印结果:

    [
        {
            "uid": "rc-upload-1551084269812-3",
            "width": 1000,
            "height": 667,
            "lastModified": 1501414799000,
            "lastModifiedDate": "2017-07-30T11:39:59.000Z",
            "name": "29381f30e924b89914e91b33.jpg",
            "size": 135204,
            "type": "image/jpeg",
            "percent": 100,
            "originFileObj": {
                "uid": "rc-upload-1551084269812-3",
                "width": 1000,
                "height": 667
            },
            "status": "done",
            "thumbUrl": "data:image/jpeg;base64,/E3ju1tlaK1fzJOnHQU3LsLV7HO6Zrk11MZJ7luT0A4FZuRagi9quvzQQ4iuEJ7ZpqTG4djDsPFl2Lg733f8C4q+YhQ8zoYfGSqoMmfwo5huLL0HjiyPDSYPvxRdC1XQvxeLrB8fvl/OnoLmL9vrdvvYS3NGFVe2YsASOh71JfQyrqV2mXLHOcccVSIYEnDyZO9XXB9KYH//Z",
            "response": {
                "retCode": 0,
                "msg": "success",
                "imgUrl": "http://qianguyihao.com/hfwpjouiurewnmbhepr689.jpg",
            }
        },
        {
            "uid": "rc-upload-1551084269812-5",
            "width": 600,
            "height": 354,
            "lastModified": 1546701318000,
            "lastModifiedDate": "2019-01-05T15:15:18.000Z",
            "name": "e30e7b9680634b2c888c8bb513cc595d.jpg",
            "size": 31731,
            "type": "image/jpeg",
            "percent": 100,
            "originFileObj": {
                "uid": "rc-upload-1551084269812-5",
                "width": 600,
                "height": 354
            },
            "status": "done",
            "thumbUrl": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAHQ9qKKlbimcXrIH9o2vH/AC2T+ddPj98v+9RRWsuhnHdk0ar9qb5R0Pb6VPB/qh9aKKiRr0Irnt/vUDr+NFFJCRqWxJik5Pb+dLJ938aKK06mYSdKKKKBH//Z",
            "response": {
                "retCode": 0,
                "imgUrl": "http://qianguyihao.com/opfewfwj098902kpkpkkj976fe.jpg",
                "photoid": 271850
            }
        }
    ]
    

    上方的json数据中,需要做几点解释:

    (1)response 字段里面的数据,就是请求接口后,后台返回给前端的数据,里面包含了图片的url链接。

    (2)status 字段里存放的是图片上传的实时状态,包括上传中、上传完成、上传失败。

    (3)thumbUrl字段里面存放的是图片的base64编码。

    这个base64编码非常非常长。当点击图片预览的时候,其实就是加载的 thumbUrl 这个字段里的资源,难怪浏览器会卡死。

    解决办法:在 handleChange方法里,图片上传成功后,将 thumbUrl 字段里面的 base64 编码改为真实的图片url。代码实现如下:

      handleChange = ({ file, fileList }) => {
        console.log(JSON.stringify(file)); // file 是当前正在上传的 单个 img
        console.log(JSON.stringify(fileList)); // fileList 是已上传的全部 img 列表
    
    
        // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。
        // 图片上传成功后,fileList数组中的 thumbUrl 中保存的是图片的base64字符串,这种情况,导致的问题是:图片上传成功后,点击图片缩略图,浏览器会会卡死。而下面这行代码,可以解决该bug。
        fileList.forEach(imgItem => {
          if (imgItem && imgItem.status == 'done' && imgItem.response && imgItem.response.imgUrl) {
            imgItem.thumbUrl = imgItem.response.imgUrl;
          }
        });
    
        this.setState({
          imgList: fileList,
        });
      };
    

    新需求:编辑现有页面

    上面一段的代码中,我们是在新建的页面中,从零开始上传图片。

    现在有个新的需求:如何编辑现有的页面呢?也就是说,现有的页面在初始化时,是默认有几张图片的。当我编辑这个页面时,可以对现有的图片做增删,也能增加新的图片。而且要保证:新建页面和编辑现有页面,是共用一套代码。

    我看到upload 组件有提供 defaultFileList 的属性。我试了下,这个defaultFileList 的属性根本没法儿用。

    那就只要手动实现了。我的model层代码,是用 redux 写的。整体的实现思路如下:(这个也是在真正在实战中用到的代码)

    (1)PicturesWall.js:

    /* eslint-disable */
    
    import { Upload, Icon, Modal, Form } from 'antd';
    
    const FormItem = Form.Item;
    
    class PicturesWall extends PureComponent {
      state = {
        previewVisible: false,
        previewImage: '',
      };
    
      // 页面初始化的时候,从接口拉取默认的图片数据
      componentDidMount() {
        const { dispatch } = this.props;
        dispatch({
          type: 'mymodel/getAllInfo',
          payload: { params: xxx },
        });
      }
    
      handleChange = ({ file, fileList }) => {
        const { dispatch } = this.props;
        // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。
        // 图片上传成功后,fileList数组中的 thumbUrl 中保存的是图片的base64字符串,这种情况,导致的问题是:图片上传成功后,点击图片缩略图,浏览器会会卡死。而下面这行代码,可以解决该bug。
        fileList.forEach(imgItem => {
          if (imgItem && imgItem.status == 'done' && imgItem.response && imgItem.response.imgUrl) {
            imgItem.thumbUrl = imgItem.response.imgUrl;
          }
        });
    
        dispatch({
          type: 'mymodel/setImgList',
          payload: fileList,
        });
      };
    
      handleCancel = () => this.setState({ previewVisible: false });
    
      handlePreview = file => {
        this.setState({
          previewImage: file.url || file.thumbUrl,
          previewVisible: true,
        });
      };
    
      // 参考链接:https://www.jianshu.com/p/f356f050b3c9
      handleBeforeUpload = file => {
        //限制图片 格式、size、分辨率
        const isJPG = file.type === 'image/jpeg';
        const isJPEG = file.type === 'image/jpeg';
        const isGIF = file.type === 'image/gif';
        const isPNG = file.type === 'image/png';
        const isLt2M = file.size / 1024 / 1024 < 2;
    
        if (!(isJPG || isJPEG || isGIF || isPNG)) {
          Modal.error({
            title: '只能上传JPG 、JPEG 、GIF、 PNG格式的图片~',
          });
        } else if (!isLt2M) {
          Modal.error({
            title: '超过2M限制,不允许上传~',
          });
        }
    
      }
    
        // 参考链接:https://github.com/ant-design/ant-design/issues/8779
        return new Promise((resolve, reject) => {
          if (!(isJPG || isJPEG || isGIF || isPNG)) {
            reject(file);
          } else {
            resolve(file && this.checkImageWH(file));
          }
        });
    
      };
    
      //返回一个 promise:检测通过则返回resolve;失败则返回reject,并阻止图片上传
      checkImageWH(file) {
        let self = this;
        return new Promise(function(resolve, reject) {
          let filereader = new FileReader();
          filereader.onload = e => {
            let src = e.target.result;
            const image = new Image();
            image.onload = function() {
              // 获取图片的宽高,并存放到file对象中
              console.log('file width :' + this.width);
              console.log('file height :' + this.height);
              file.width = this.width;
              file.height = this.height;
              resolve();
            };
            image.onerror = reject;
            image.src = src;
          };
          filereader.readAsDataURL(file);
        });
      }
    
      handleSubmit = e => {
        const { dispatch, form } = this.props;
        e.preventDefault();
    
        const {
          mymodel: { imgList }, // 从props中拿默认的图片数据
        } = this.props;
    
        form.validateFieldsAndScroll((err, values) => {
          // values 是form表单里的参数
          // 点击按钮后,将表单提交给后台
    
    
          // start 问题描述:当编辑现有页面时,如果针对已经存在的默认图片不做修改,则不会触发 upload 的 onChange方法。此时提交表单,表单里的 myImg 字段是空的。
          // 解决办法:如果发现存在默认图片,则追加到表单中
          if (!values.myImg) {
    
            values.myImg = { fileList: [] };
    
            values.myImg.fileList = imgList;
          }
          // end
    
          dispatch({
            type: 'mymodel/submitFormData',
            payload: values,
          });
        });
      };
    
      render() {
        const { previewVisible, previewImage } = this.state; //  从 state 中拿数据
    
        const {
          mymodel: { imgList }, // 从props中拿到的图片数据
        } = this.props;
    
        const uploadButton = (
          <div>
            <Icon type="plus" />
            <div className="ant-upload-text">Upload</div>
          </div>
        );
        return (
          <div className="clearfix">
            <Form onSubmit={this.handleSubmit} hideRequiredMark style={{ marginTop: 8 }}>
              <FormItem label="图片上传" {...formItemLayout}>
                {getFieldDecorator('myImg')(
                  <Upload
                    action="//jsonplaceholder.typicode.com/posts/" // 这个是接口请求,实际开发中,要替换成你自己的业务接口
                    data={file => ({
                      // data里存放的是接口的请求参数
                      param1: myParam1,
                      param2: myParam2,
                      photoCotent: file, // file 是当前正在上传的图片
                      photoWidth: file.height, // 通过  handleBeforeUpload 获取 图片的宽高
                      photoHeight: file.width,
                    })}
                    listType="picture-card"
                    fileList={imgList}  // 改为从 props 里拿图片数据,而不是从 state
                    onPreview={this.handlePreview} // 点击图片缩略图,进行预览
                    beforeUpload={this.handleBeforeUpload} // 上传之前,对图片的格式做校验,并获取图片的宽高
                    onChange={this.handleChange} // 每次上传图片时,都会触发这个方法
                  >
                    {this.state.imgList.length >= 9 ? null : uploadButton}
                  </Upload>
                )}
              </FormItem>
            </Form>
            <Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
              <img alt="example" style={{  '100%' }} src={previewImage} />
            </Modal>
          </div>
        );
      }
    }
    
    export default PicturesWall;
    
    

    (2)mymodel.js:

    /* eslint-disable */
    
    import { routerRedux } from 'dva/router';
    import { message, Modal } from 'antd';
    import {
      getGoodsInfo,
      getAllGoods,
    } from '../services/api';
    import { trim, getCookie } from '../utils/utils';
    
    export default {
      namespace: 'mymodel',
    
      state: {
        form: {},
        list: [],
        listDetail: [],
        goodsList: [],
        goodsListDetail: [],
        pagination: {
          pageSize: 10,
          total: 0,
          current: 1,
        },
        imgList: [], //图片
      },
      subscriptions: {
        setup({ dispatch, history }) {
          history.listen(location => {
            if (location.pathname !== '/xx/xxx') return;
            if (!location.state || !location.state.xxxId) return;
            dispatch({
              type: 'fetch',
              payload: location.state,
            });
          });
        },
      },
    
      effects: {
        // 接口。获取所有工厂店的列表 (步骤02)
        *getAllInfo({ payload }, { select, call, put }) {
          yield put({
            type: 'form',
            payload,
          });
          console.log('params:' + JSON.stringify(payload));
    
          let params = {};
          params = payload;
    
          const response = yield call(getGoodsInfo, params);
    
          console.log('smyhvae response:' + JSON.stringify(response));
          if (response.error) return;
          yield put({
            type: 'allInfo',
            payload:
              (response.data &&
                response.data.map(item => ({
                  xx1: item.yy1,
                  xx2: item.yy2,
                }))) ||
              [],
          });
    
          // response 里包含了接口返回给前端的默认图片数据
          if (response && response.data && response.data[0] && response.data[0].my_jpg) {
            let tempImgList = response.data[0].my_jpg.split(',');
            let imgList = [];
    
            if (tempImgList.length > 0) {
              tempImgList.forEach(item => {
                imgList.push({
                  uid: item,
                  name: 'xxx.png',
                  status: 'done',
                  thumbUrl: item,
                });
              });
            }
    
            // 通过  redux的方式 将 默认图片 传给 imgList
            console.log('smyhvae payload imgList:' + JSON.stringify(imgList));
            yield put({
              type: 'setImgList',
              payload: imgList,
            });
          }
        },
    
        *setImgList({ payload }, { call, put }) {
          console.log('model setImgList');
          yield put({
            type: 'getImgList',
            payload,
          });
        },
      },
    
      reducers: {
        allInfo(state, action) {
          return {
            ...state,
            list: action.payload,
          };
        },
        getImgList(state, action) {
          return {
            ...state,
            imgList: action.payload,
          };
        },
      },
    };
    
    

    上面的代码,可以规避 upload 组件的一些bug;而且可以在上传前,通过校验图片的尺寸、大小等,如果不满足条件,则弹出modal弹窗,阻止上传。

    大功告成。本文感谢 ld 同学的支持。

    其他问题

    最后一段

    有人说,前端开发,连卖菜的都会。可如果真的遇到技术难题,还是得找个靠谱的前端同学才行。这不,来看看前端码农日常:

  • 相关阅读:
    XML基础总结
    异常处理
    集合总结
    事件源与监听器
    JAVA中的GUI---swing 和awt
    docker搭建wordpress
    Jenkins--第四关_扩展
    docker 安装centos 7
    Jenkins_第五关_系统管理(1)
    Jenkins--第三关_Gitlab安装和配置(续)
  • 原文地址:https://www.cnblogs.com/qianguyihao/p/10460834.html
Copyright © 2011-2022 走看看