前言
本次做后台管理系统, 采用的是 AntD 框架. 涉及到图片的上传, 用的是 AntD 的 https://ant.design/components/upload-cn/ 组件.
前端做文件上传这个功能, 是很有技术难度的. 既然框架给我们提供好了, 那就直接用呗. 结果用的时候, 发现 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={{ 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 {
- 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 同学的支持.
最后一段
有人说, 前端开发, 连卖菜的都会. 可如果真的遇到技术难题, 还是得找个靠谱的前端同学才行. 这不, 来看看前端码农日常:
来源: https://www.cnblogs.com/qianguyihao/p/10460834.html