title: 09-AntD框架的upload组件上传图片时遇到的一些坑
publish: true
前言
本次做后台管理系统,采用的是 AntD 框架。涉及到图片的上传,用的是AntD的 upload 组件。
前端做文件上传这个功能,是很有技术难度的。既然框架给我们提供好了,那就直接用呗。结果用的时候,发现 upload 组件的很多bug。下面来列举几个。
备注:本文写于2019-03-02,使用的 antd 版本是 3.13.6。
使用 AntD 的 upload 组件做图片的上传
因为需要上传多张图片,所以采用的是照片墙的形式。上传成功后的界面如下:
(1)上传中:
(2)上传成功:
(3)图片预览:
按照官方提供的实例,特此整理出项目开发中的完整写法,亲测有效。代码如下:
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)); console.log(JSON.stringify(fileList));
this.setState({ imgList: fileList, }); };
handleCancel = () => this.setState({ previewVisible: false });
handlePreview = file => { this.setState({ previewImage: file.url || file.thumbUrl, previewVisible: true, }); };
handleBeforeUpload = file => { 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); };
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() { 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) => { dispatch({ type: 'mymodel/submitFormData', payload: values, }); }); };
render() { const { previewVisible, previewImage, imgList } = this.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)); console.log(JSON.stringify(fileList));
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:
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; 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, }); };
handleBeforeUpload = file => { 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限制,不允许上传~', }); }
}
return new Promise((resolve, reject) => { if (!(isJPG || isJPEG || isGIF || isPNG)) { reject(file); } else { resolve(file && this.checkImageWH(file)); } });
};
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() { 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 }, } = this.props;
form.validateFieldsAndScroll((err, values) => {
if (!values.myImg) {
values.myImg = { fileList: [] };
values.myImg.fileList = imgList; }
dispatch({ type: 'mymodel/submitFormData', payload: values, }); }); };
render() { const { previewVisible, previewImage } = this.state;
const { mymodel: { imgList }, } = 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:
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: { *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, }))) || [], });
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, }); }); }
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 同学的支持。
更多内容,可以看本人的另外一篇文章:
其他问题
最后一段
有人说,前端开发,连卖菜的都会。可如果真的遇到技术难题,还是得找个靠谱的前端同学才行。这不,来看看前端码农日常: