欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

AntD框架的upload组件上传图片时遇到的一些坑

程序员文章站 2022-05-27 22:34:13
前言 本次做后台管理系统,采用的是 AntD 框架。涉及到图片的上传,用的是AntD的 "upload" 组件。 前端做文件上传这个功能,是很有技术难度的。既然框架给我们提供好了,那就直接用呗。结果用的时候,发现 upload 组件的很多bug。下面来列举几个。 备注:本文写于2019 03 02, ......

前言

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

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

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

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

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

(1)上传中:

AntD框架的upload组件上传图片时遇到的一些坑

(2)上传成功:

AntD框架的upload组件上传图片时遇到的一些坑

(3)图片预览:

AntD框架的upload组件上传图片时遇到的一些坑

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

/* 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={{ width: '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 列表

    if (file && file.response && file.response.retcode == 0) {
      console.log('图片上传成功');
      filelist.foreach(item => {
        // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。
        // 图片上传成功后,filelist数组中的 thumburl 中保存的是图片的base64字符串,这种情况,导致的问题是:图片上传成功后,点击图片缩略图,浏览器会会卡死。而下面这行代码,可以解决该bug。
        item.thumburl = item.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;
    if (file && file.response && file.response.retcode == 0) {
      console.log('图片上传成功');
      filelist.foreach(item => {
        // 【重要】将 图片的base64替换为图片的url。 这一行一定不会能少。
        // 图片上传成功后,filelist数组中的 thumburl 中保存的是图片的base64字符串,这种情况,导致的问题是:图片上传成功后,点击图片缩略图,浏览器会会卡死。而下面这行代码,可以解决该bug。
        item.thumburl = item.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';
    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 } = 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={{ width: '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,
      };
    },
  },
};

大功告成。

本文感谢 ld 同学的支持。

最后一段

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

AntD框架的upload组件上传图片时遇到的一些坑