Vue + Element UI + Koa 實(shí)現(xiàn)多圖片+數(shù)據(jù)上傳并保存圖片到本地

一、寫作背景

最近在用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)

https://element.eleme.cn/#/zh-CN/component/layout

Element UI Github

https://github.com/ElemeFE/element

四、多圖片上傳的流程

  • 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í)修改的 _
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,628評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,642評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,444評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,948評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,185評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,794評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,418評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,414評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,750評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • 基于Vue的一些資料 內(nèi)容 UI組件 開(kāi)發(fā)框架 實(shí)用庫(kù) 服務(wù)端 輔助工具 應(yīng)用實(shí)例 Demo示例 element★...
    嘗了又嘗閱讀 1,164評(píng)論 0 1
  • UI組件 element- 餓了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的組件庫(kù) m...
    柴東啊閱讀 15,867評(píng)論 2 140
  • UI組件 element- 餓了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的組件庫(kù) m...
    小姜先森o0O閱讀 9,564評(píng)論 0 72
  • UI組件 element- 餓了么出品的Vue2的web UI工具套件 Vux- 基于Vue和WeUI的組件庫(kù) m...
    你猜_3214閱讀 11,102評(píng)論 0 118
  • 一件絕美的白色婚紗掛在玻璃展示柜里,整個(gè)房間闊氣干凈,一眼望去只有這件婚紗,孤獨(dú)又傲然地立在那里。 門“吱”的一聲...
    黍小藜閱讀 937評(píng)論 0 6