一、写作背景
最近在用Vue写一个仿京东、淘宝的电商项目过程中踩了一个大坑 ---- 多图片上传 + 保存
二、问题描述
- 电商项目其中一个较为核心的功能当然就是商品的添加了,而添加商品势必涉及到图片的上传。
- 而一种商品很明显不止一张图片,其实严格来说大概要15张,因为其中不光要有缩略图+正常图,还有一个放大镜的功能要实现,当然,我们这里暂时不考虑性能的问题,只要求5张图片
- 不过,即使是5张,也涉及到了多图片上传的问题。虽然element UI本身支持多图片上传,但是其内部机制是每张图片发送一个http请求的,这不是我们想要的
- 这个问题卡了我不少时间,期间找了不少资料,然并软
- 对于一个上线的项目来说,我觉得图片应该是有图片服务器的,如果仔细看一下就会发现京东、淘宝的图片地址都是网络地址,直接从服务器请求过来的,这种情况其实就很简单,不过对我们初学者练手来说,这不切实际,毕竟租服务器是要钱的嘛
三、项目介绍及使用的工具
- 这个项目采用的是前后端分离的方式写的
- 前端使用的是Vue.js,用了vue-cli 3.x
- 后台管理同样使用的是Vue
- 服务端使用的是Node.js,采用了我比较熟悉的Koa框架(跟Express差不多,开发团队都一样)
- 跨域问题的解决方法使用的是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发送http请求,并将文件数据发送到服务端
- 4、服务端接收数据,并使用koa-multer将文件存储到本地
- 5、获取图片的路径,将路径存到数据库,需要的时候提取出来返回到前端
- 6、前端根据后端返回的图片路径再进行合适的处理将图片展示到页面
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" // 限制上传文件最大数量为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">立即创建</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() {
// 获取到 上传的所有文件,它是一个数组
const fileArray = this.$refs.upload.uploadFiles;
// 实例化FormData对象
const fd = new FormData();
// 遍历文件数组,将所有文件存入fd中
for(let i = 0; i < fileArray.length; i++) {
// 在这里数组每一项的.raw才是你需要的文件,有疑惑的可以打印到控制台看一下就清楚了
fd.append('avatar', fileArray[i].raw);
}
// 发送HTTP请求,发送数据
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) {
// 设置文件的存储目录,需提前创建
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: '添加成功'}; //返回数据
})
- 上面代码中的upload.array('avatar', 5)就是koa-multer的使用了,程序进行到这里,就会将你上传的图片保存到本地了,
- 其中'avatar'就是前端fd.append('avatar', fileArray[i].raw);中的'avatar',这个字段名换了,服务端的就也要换
- 而数字5则是用来限制文件个数 的
7、携带form表单中的数据一起上传
针对这个需求,element UI 提供了data属性,用于上传携带的数据,但是我们用不到,因为我们的数据是自己发送http请求自己上传的。
这个问题也困扰了我不少时间,其原因可能是我一开始就想岔了,
7.1 当时我有两个想法:
它们的依据都是这个:
const files = ctx.req.files; //上传过来的文件
const data = ctx.request.body; // 上传的数据
当发送的是文件时, files !== undefined , data === {};
当发送的是数据时, files === undefined , data !== {}
- 1、发送两次请求,一次传文件,一次传数据,后端通过判断files的值是否为undefined,是的话说明本次请求发送的是数据,不是的话说明发送的是图片文件,定然后义变量将对应的数据接收,然后一起存入数据库中即可
很明显这个方案是行不通的,因为每次发送http请求,此段代码都会运行一次,根本不可能同时获取到所有的数据
- 2、改进后的方案:知道了问题所在的话解决就很容易了,当时我就采用了一个特别笨的办法 ---- 一次添加数据、一次更新数据,第二次请求更新数据的时候还得先获取到该数据的id,
当然,方法虽然很笨,但是是能解决问题的,即使这很不可取,但是也不失为一种解决方案
7.2 更加优雅的做法
上面那种方法很明显不好,太浪费资源了,而且还很慢,一旦项目大一点就炸了,所幸我后来在做搜索功能的时候想到了一种更好的办法,这种办法其实我之前在写论坛项目的时候经常用,但是不知道为什么这次没想到,失败啊失败
他就是:通过params发送数据,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 // 将数据放在就可以上传到服务端
}).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里的数据
const data = ctx.query; // 上传的数据
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: '添加成功'};
})
八、结束语
- 以上就是此次的全部内容了,希望对你有所帮助,如有错误,欢迎指正,我会及时修改的 _