一、寫作背景
最近在用Vue寫一個(gè)仿京東、淘寶的電商項(xiàng)目過(guò)程中踩了一個(gè)大坑 ---- 多圖片上傳 + 保存
二、問(wèn)題描述
- 電商項(xiàng)目其中一個(gè)較為核心的功能當(dāng)然就是商品的添加了,而添加商品勢(shì)必涉及到圖片的上傳。
- 而一種商品很明顯不止一張圖片,其實(shí)嚴(yán)格來(lái)說(shuō)大概要15張,因?yàn)槠渲胁还庖锌s略圖+正常圖,還有一個(gè)放大鏡的功能要實(shí)現(xiàn),當(dāng)然,我們這里暫時(shí)不考慮性能的問(wèn)題,只要求5張圖片
- 不過(guò),即使是5張,也涉及到了多圖片上傳的問(wèn)題。雖然element UI本身支持多圖片上傳,但是其內(nèi)部機(jī)制是每張圖片發(fā)送一個(gè)http請(qǐng)求的,這不是我們想要的
- 這個(gè)問(wèn)題卡了我不少時(shí)間,期間找了不少資料,然并軟
- 對(duì)于一個(gè)上線的項(xiàng)目來(lái)說(shuō),我覺(jué)得圖片應(yīng)該是有圖片服務(wù)器的,如果仔細(xì)看一下就會(huì)發(fā)現(xiàn)京東、淘寶的圖片地址都是網(wǎng)絡(luò)地址,直接從服務(wù)器請(qǐng)求過(guò)來(lái)的,這種情況其實(shí)就很簡(jiǎn)單,不過(guò)對(duì)我們初學(xué)者練手來(lái)說(shuō),這不切實(shí)際,畢竟租服務(wù)器是要錢的嘛
三、項(xiàng)目介紹及使用的工具
- 這個(gè)項(xiàng)目采用的是前后端分離的方式寫的
- 前端使用的是Vue.js,用了vue-cli 3.x
- 后臺(tái)管理同樣使用的是Vue
- 服務(wù)端使用的是Node.js,采用了我比較熟悉的Koa框架(跟Express差不多,開(kāi)發(fā)團(tuán)隊(duì)都一樣)
- 跨域問(wèn)題的解決方法使用的是Vue提供的方法,配置項(xiàng)目目錄下的vue.config.js文件即可,如果沒(méi)有就新建一個(gè),具體配置這里就不一一贅述了,有需要的話可以找我
- 存儲(chǔ)文件使用的是koa-multer中間件
- HTTP請(qǐng)求: axios
- 圖片上傳使用的是:Element UI uploads組件
Element UI 中文站點(diǎn)
Element UI Github
四、多圖片上傳的流程
- 1、使用Element UI 的uploads組件獲取需要上傳的圖片(別忘了配置支持多文件上傳的屬性)
- 2、使用HTML5提供的FormData將文件添加進(jìn)去
- 3、使用axios發(fā)送http請(qǐng)求,并將文件數(shù)據(jù)發(fā)送到服務(wù)端
- 4、服務(wù)端接收數(shù)據(jù),并使用koa-multer將文件存儲(chǔ)到本地
- 5、獲取圖片的路徑,將路徑存到數(shù)據(jù)庫(kù),需要的時(shí)候提取出來(lái)返回到前端
- 6、前端根據(jù)后端返回的圖片路徑再進(jìn)行合適的處理將圖片展示到頁(yè)面
5、前端代碼及解析
<template >
<div id="goods-add">
<el-form :model="goodinfo" ref="goodinfo" label-width="100px" class="demo-ruleForm">
<el-form-item label="名字">
<el-input v-model="goodinfo.name"></el-input>
</el-form-item>
<el-form-item label="價(jià)格">
<el-input v-model="goodinfo.price"></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="goodinfo.description"></el-input>
</el-form-item>
<el-form-item label="品牌">
<el-input v-model="goodinfo.brand"></el-input>
</el-form-item>
<el-form-item label="標(biāo)簽">
<el-input v-model="goodinfo.label" placeholder="每個(gè)標(biāo)簽使用 分開(kāi)"></el-input>
</el-form-item>
<div class="img-upload">
<el-upload
action="#" // 上傳地址,這里我們手動(dòng)上傳,所以不需要填寫地址
:limit="5" // 限制上傳文件最大數(shù)量為5
ref="upload" //標(biāo)記,我覺(jué)得相當(dāng)于id,可用來(lái)選取元素
:multiple="true" // 開(kāi)啟多文件上傳
:auto-upload="false" //關(guān)閉自動(dòng)上傳
:file-list="fileList" // 上傳文件列表
list-type="picture-card"> // 上傳文件的展示形式,這個(gè)是卡片
<el-button slot="trigger" size="small" type="primary">選取文件</el-button>
<div slot="tip" class="el-upload__tip">上傳圖片大小不超過(guò)500kb</div>
</el-upload>
</div>
<el-form-item>
<el-button type="primary" @click="submitUpload">立即創(chuàng)建</el-button>
<el-button @click="resetForm('goodinfo')">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'goods-add',
methods: {
submitUpload() {
// 獲取到 上傳的所有文件,它是一個(gè)數(shù)組
const fileArray = this.$refs.upload.uploadFiles;
// 實(shí)例化FormData對(duì)象
const fd = new FormData();
// 遍歷文件數(shù)組,將所有文件存入fd中
for(let i = 0; i < fileArray.length; i++) {
// 在這里數(shù)組每一項(xiàng)的.raw才是你需要的文件,有疑惑的可以打印到控制臺(tái)看一下就清楚了
fd.append('avatar', fileArray[i].raw);
}
// 發(fā)送HTTP請(qǐng)求,發(fā)送數(shù)據(jù)
axios({
url: '/api/view/add-good',
method: 'post',
data: fd,
}).then(res => {
console.log(res.data);
})
}
}
}
</script>
六、后端Koa使用koa-multer接收文件并保存
6.1 koa-multer的安裝與配置
- 安裝: npm install --save koa-multer
- 配置:
const multer = require('koa-multer');
const storage = multer.diskStorage({
destination (req, file, cb) {
// 設(shè)置文件的存儲(chǔ)目錄,需提前創(chuàng)建
cb(null, '../mall-view/src/assets/img')
},
filename (req, file, cb) {
// 設(shè)置 文件名
const name = file.originalname;
// 設(shè)置文件的后綴名,
//我這里取的是上傳文件的originalname屬性的后四位,
// 即: .png,.jpg等,這樣就需要上傳文件的后綴名為3位
const extension = name.substring(name.length - 4);
cb(null, 'img-' + Date.now() + extension);
}
})
const upload = multer({ storage: storage })
6.2 使用
router.post('/view/add-good', upload.array('avatar', 5), async (ctx) => {
const files = ctx.req.files; //上傳過(guò)來(lái)的文件
ctx.body = {msg: '添加成功'}; //返回?cái)?shù)據(jù)
})
- 上面代碼中的upload.array('avatar', 5)就是koa-multer的使用了,程序進(jìn)行到這里,就會(huì)將你上傳的圖片保存到本地了,
- 其中'avatar'就是前端fd.append('avatar', fileArray[i].raw);中的'avatar',這個(gè)字段名換了,服務(wù)端的就也要換
- 而數(shù)字5則是用來(lái)限制文件個(gè)數(shù) 的
7、攜帶form表單中的數(shù)據(jù)一起上傳
針對(duì)這個(gè)需求,element UI 提供了data屬性,用于上傳攜帶的數(shù)據(jù),但是我們用不到,因?yàn)槲覀兊臄?shù)據(jù)是自己發(fā)送http請(qǐng)求自己上傳的。
這個(gè)問(wèn)題也困擾了我不少時(shí)間,其原因可能是我一開(kāi)始就想岔了,
7.1 當(dāng)時(shí)我有兩個(gè)想法:
它們的依據(jù)都是這個(gè):
const files = ctx.req.files; //上傳過(guò)來(lái)的文件
const data = ctx.request.body; // 上傳的數(shù)據(jù)
當(dāng)發(fā)送的是文件時(shí), files !== undefined , data === {};
當(dāng)發(fā)送的是數(shù)據(jù)時(shí), files === undefined , data !== {}
- 1、發(fā)送兩次請(qǐng)求,一次傳文件,一次傳數(shù)據(jù),后端通過(guò)判斷files的值是否為undefined,是的話說(shuō)明本次請(qǐng)求發(fā)送的是數(shù)據(jù),不是的話說(shuō)明發(fā)送的是圖片文件,定然后義變量將對(duì)應(yīng)的數(shù)據(jù)接收,然后一起存入數(shù)據(jù)庫(kù)中即可
很明顯這個(gè)方案是行不通的,因?yàn)槊看伟l(fā)送http請(qǐng)求,此段代碼都會(huì)運(yùn)行一次,根本不可能同時(shí)獲取到所有的數(shù)據(jù)
- 2、改進(jìn)后的方案:知道了問(wèn)題所在的話解決就很容易了,當(dāng)時(shí)我就采用了一個(gè)特別笨的辦法 ---- 一次添加數(shù)據(jù)、一次更新數(shù)據(jù),第二次請(qǐng)求更新數(shù)據(jù)的時(shí)候還得先獲取到該數(shù)據(jù)的id,
當(dāng)然,方法雖然很笨,但是是能解決問(wèn)題的,即使這很不可取,但是也不失為一種解決方案
7.2 更加優(yōu)雅的做法
上面那種方法很明顯不好,太浪費(fèi)資源了,而且還很慢,一旦項(xiàng)目大一點(diǎn)就炸了,所幸我后來(lái)在做搜索功能的時(shí)候想到了一種更好的辦法,這種辦法其實(shí)我之前在寫論壇項(xiàng)目的時(shí)候經(jīng)常用,但是不知道為什么這次沒(méi)想到,失敗啊失敗
他就是:通過(guò)params發(fā)送數(shù)據(jù),axios支持這個(gè)
所以,改進(jìn)后的代碼如下:
前端:
submitUpload() {
const session = this.$session.getAll();
const boss = session.userinfo;
const goodinfo = this.goodinfo;
axios({ // 之所以要寫這個(gè)請(qǐng)求,是因?yàn)槲倚枰@取添加商品的商家信息
method: 'post',
url: '/api/view/getstore',
data: { boss_id: boss.boss_id}
}).then(res => {
if(res.status === 200) {
const store_id = res.data.id;
const store_name = res.data.name;
const boss_id = boss.boss_id;
const boss_name = boss.username;
const name = goodinfo.name;
const new_price = goodinfo.price;
const description = goodinfo.description;
const brand = goodinfo.brand;
const label = goodinfo.label;
const data = {
store_id: store_id,
store_name: store_name,
boss_id: boss_id,
boss_name: boss_name,
name: name,
new_price: new_price,
description: description,
brand: brand,
label: label
};
const fileArray = this.$refs.upload.uploadFiles;
const fd = new FormData();
for(let i = 0; i < fileArray.length; i++) {
fd.append('avatar', fileArray[i].raw);
}
axios({
url: '/api/view/add-good',
method: 'post',
data: fd,
params: data // 將數(shù)據(jù)放在就可以上傳到服務(wù)端
}).then(res => {
console.log(res.data);
})
}
})
},
后端:
router.post('/view/add-good', upload.array('avatar', 5), async (ctx) => {
const files = ctx.req.files; //上傳過(guò)來(lái)的文件
// 服務(wù)端通過(guò)ctx.query 可以獲得前端axios中的params里的數(shù)據(jù)
const data = ctx.query; // 上傳的數(shù)據(jù)
const img_1 = files[0].path;
const img_2 = files[1].path;
const img_3 = files[2].path;
const img_4 = files[3].path;
const img_5 = files[4].path;
const store_id = data.store_id;
const store_name = data.store_name;
const boss_id = data.boss_id;
const boss_name = data.boss_name;
const name = data.name;
const new_price = data.new_price;
const description = data.description;
const brand = data.brand;
const label = data.label;
const data1 = [store_id, store_name, boss_id, boss_name, name, new_price, description, brand, img_1, img_2, img_3, img_4, img_5, label];
await editGood.addGood(data1);
ctx.body = {msg: '添加成功'};
})
八、結(jié)束語(yǔ)
- 以上就是此次的全部?jī)?nèi)容了,希望對(duì)你有所幫助,如有錯(cuò)誤,歡迎指正,我會(huì)及時(shí)修改的 _