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

一、寫作背景

最近在用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 中文站點

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

Element UI Github

https://github.com/ElemeFE/element

四、多圖片上傳的流程

  • 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)容了,希望對你有所幫助,如有錯誤,歡迎指正,我會及時修改的 _
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

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