diff --git a/13-React基础/09-AntD框架的upload组件上传图片时遇到的一些坑.md b/13-React基础/09-AntD框架的upload组件上传图片时遇到的一些坑.md new file mode 100644 index 0000000..1b74e34 --- /dev/null +++ b/13-React基础/09-AntD框架的upload组件上传图片时遇到的一些坑.md @@ -0,0 +1,590 @@ + +## 前言 + +本次做后台管理系统,采用的是 AntD 框架。涉及到图片的上传,用的是AntD的 [upload](https://ant.design/components/upload-cn/) 组件。 + +前端做文件上传这个功能,是很有技术难度的。既然框架给我们提供好了,那就直接用呗。结果用的时候,发现 upload 组件的很多bug。 + +## 使用 AntD 的 upload 组件做图片的上传 + +因为需要上传多张图片,所以采用的是照片墙的形式。上传成功后的界面如下: + +按照官方提供的实例,特此整理出项目开发中的完整写法,亲测有效。代码如下: + +```javascript +/* 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 = ( +
+ +
Upload
+
+ ); + return ( +
+
+ + {getFieldDecorator('myImg')( + ({ // 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} + + )} + +
+ + example + +
+ ); + } +} + +export default PicturesWall; + +``` + + +## 上传后,点击图片预览,浏览器卡死的问题 + +依据上方的代码,通过 Antd 的 upload 组件将图片上传成功后,点击图片的缩略图,理应可以在当前页面弹出 Modal,预览图片。但实际的结果是,浏览器一定会卡死。 + +定位问题发现,原因竟然是:图片上传成功后, upload 会将其转为 base64编码。base64这个字符串太大了,点击图片预览的时候,浏览器在解析一大串字符串,然后就卡死了。详细过程描述如下。 + +上方代码中,我们可以把 handleChange(file, fileList)方法中的 `file`、以及 `fileList`打印出来看看。 `file`指的是当前正在上传的 单个 img,`fileList`是已上传的全部 img 列表。 当我上传完 两张图片后, 打印结果如下: + + +file的打印的结果如下: + +```json + { + "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 的打印结果: + +```json +[ + { + "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。代码实现如下: + +```javascript + 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, + }); + }; +``` + + +## 新需求:编辑现有页面 + +上面一段的代码中,我们是在新建的页面中,从零开始上传页面。 + +现在有个新的需求:如何编辑现有的页面呢?也就是说说,现有的页面中,是默认有几张图片的。当我编辑这个页面时,可以对现有的图片做增删,也能增加新的图片。 + +我的model层代码,是用 redux 写的。实现思路如下: + + +(1)PicturesWall.js: + +```javascript +/* 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 = ( +
+ +
Upload
+
+ ); + return ( +
+
+ + {getFieldDecorator('myImg')( + ({ + // 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} + + )} + +
+ + example + +
+ ); + } +} + +export default PicturesWall; + +``` + +(2)mymodel.js: + +```javascript +/* eslint-disable */ + +import { routerRedux } from 'dva/router'; +import { message, Modal } from 'antd'; +import { + getTuanList, + getAllFactory, + getAllFactGoods, + createFactShop, + updateFactShop, + deleteFactShop, + updateFactGoodsStatus, + queryShopDetail, + createFactGoods, +} from '../services/api'; +import { trim, getCookie } from '../utils/utils'; + +export default { + namespace: 'factory', + + 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(getAllFactory, 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('smyhvae 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 同学的支持。 + + + + + + + + + + + +