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