前言 本次做後臺管理系統,採用的是 AntD 框架。涉及到圖片的上傳,用的是AntD的 "upload" 組件。 前端做文件上傳這個功能,是很有技術難度的。既然框架給我們提供好了,那就直接用唄。結果用的時候,發現 upload 組件的很多bug。下麵來列舉幾個。 備註:本文寫於2019 03 02, ...
前言
本次做後臺管理系統,採用的是 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={{ 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 列表
// 【重要】將 圖片的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={{ 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,
};
},
},
};
上面的代碼,可以規避 upload 組件的一些bug;而且可以在上傳前,通過校驗圖片的尺寸、大小等,如果不滿足條件,則彈出modal彈窗,阻止上傳。
大功告成。本文感謝 ld 同學的支持。
其他問題
最後一段
有人說,前端開發,連賣菜的都會。可如果真的遇到技術難題,還是得找個靠譜的前端同學才行。這不,來看看前端碼農日常: