需求背景
最近需要做一个文件上传的功能。前端使用Vue,后端使用Node.js。
涉及技术栈
- 前端:Vue+Element-UI
- 后端:Node.js+Koa
- CDN:ali-oss
前端实现
选择element-UI自带的上传组件el-upload,限制一次只能上传1个zip格式文件,大小不超过10M。大多数上传的场景,都是在el-dialog弹窗组件中包含el-upload组件。
<template>
<el-dialog
:visible.sync="visible"
width="500px"
title="上传文件"
@before-close="onClose"
>
<el-form :model="form">
<el-form-item
label="文件上传" :label-width="formLabelWidth">
<el-upload
class="upload-demo"
:action="uploadUrl"
:limit="1"
:data="app"
:before-upload="beforeUpload"
:on-exceed="handleExceed"
:on-success="fileSuccess"
>
<el-button
size="small"
type="primary"
>点击上传</el-button>
<div
class="el-upload__tip"
slot="tip"
>请将视频压缩为zip格式上传,且大小不超过10M</div>
</el-upload>
</el-form-item>
<el-form-item>
<el-button
type="primary" @click="onSubmit">提交</el-button>
<el-button
@click="visible=false"
>取消</el-button>
</el-form-item>
</el-form>
</dialog>
</template>
<script>
// 弹窗中嵌套表单,上传组件为表单中的某一项,上传完成后要提交表单,将文件的cdn路径传至后端。
import { submit } from '@/api/upload'
// 单独在api/upload文件中写好axios请求函数submit,此处不做赘述。
export default {
name:'upload',
data() {
return {
visible: false,
form: {
filePath: null,
},
status: false,
uploadUrl:`${请求后端地址}`,
};
},
method: {
// 表单提交函数
async handleSubmit() {
// 使用status判断上传是否完成,兜底用户未上传成功就点击提交按钮的情况
if (this.status) {
this.$message.warning('文件上传中,请耐心等待');
} else {
await submit({ ...this.form});
this.$message.success('已成功提交!');
this.visible = false;
}
},
// 文件限制函数
handleExceed() {
this.$message.warning('当前限制上传 1 个文件');
},
// 上传前函数
beforeUpload(file) {
// 上传前,将状态置为true,表示处于上传过程中
this.status = true;
const fileType = file.name.substring(file.name.lastIndexOf('.') + 1);
const typeLimit = fileType === 'zip';
const sizeLimit = file.size < 10485760;
if (!typeLimit) {
this.$message({
message: '上传文件只能是 zip 格式',
type: 'warning',
});
}
if (!sizeLimit) {
this.$message({
message: '上传文件大小不能超过 10M',
type: 'warning',
});
}
return typeLimit && sizeLimit;
},
// 上传完成后函数
fileSuccess(res) {
// 上传完成后,将status置为false,表示上传过程走完
this.status = false;
if (res.code === 0) {
this.form.filePath = res.data.url;
this.$message.success('上传成功,可以提交');
} else {
this.$message.error('上传失败');
}
},
},
};
</script>
这里不得不提到el-upload组件的一个坑,当我们在el-dialog弹窗中使用这个组件的时候,el-dialog加载成功会自动触发on-success上传成功的函数。因此,只要弹窗加载成功,我们实际的文件上传成功或失败都触发的是on-success函数,on-error函数形同虚设,我们需要在on-success函数中通过返回值来判断上传成功与否。
组件上传的请求为POST请求,请求地址为uploadUrl的后端请求地址,请求参数为FormData格式,文件参数可见为:"file: (binary)",后端可解析出文件内容,并传到cdn上,返回给前端cdn地址,点击该地址可直接下载上传的文件。
后端实现
后端是Koa框架下的Node.js开发,为了处理前端传过来的FormData参数,引入了koa-body包。比较普遍的情况下,使用 koa2 的时候,处理 post 请求使用的是 koa-bodyparser包,处理图片上传使用的是 koa-multer包。而使用 koa-body包 就可以完全替代它们了。 ali-oss包是阿里云对外提供的云存储服务封装之后的 Node.js 客户端,安装之后我们可以调用阿里云的API将上传文件放在阿里云上。
要安装的包:
npm i koa-body -S
npm i ali-oss -S
使用全局引入koa-body,基本用法如下,在app.js文件中写入:
import KOA from 'koa';
import koaBody from 'koa-body';
const app = new KOA();
app.use(koaBody({
multipart: true, // 支持文件上传
}))
// 注意:在路由中间件和koa-body都存在的情况下,务必让koa-body声明在koa-router之前。否则会报错。
根据阿里云上传本地文件API文档,我们可以在service层写出阿里云的上传方法,暴露给controller层使用。
const OSS = require('ali-oss')
const client = new OSS({
bucket: '<Your BucketName>',
// cdn名称
region: '<Your Region>',
// region以杭州为例(oss-cn-hangzhou),其他region按实际情况填写
accessKeyId: '<Your AccessKeyId>',
accessKeySecret: '<Your AccessKeySecret>',
internal: false,
// 内部上传时为true,其他情况为false
});
class OssService {
async function uploadFile (path, reader) {
try {
// path为自定义的文件名称,reader为读取出来的文件内容
let result = await client.put(path, reader);
return { url: result.data }
} catch (e) {
return e.message;
}
}
}
export default OssService;
阿里云cdn的上传方法写好以后,我们在controller层处理前端发送过来的请求数据。在接口函数中,使用fs的createReadStream方法读取请求参数file的文件内容,通过数据流从文件中读取到了数据。自定义文件在cdn上的名称和存储路径,调用service中的uploadFile上传方法即可成功上传并返回cdn地址,返回给前端。
import {
route,
POST,
} from 'awilix-koa';
import OssService from '../service/upload'
import fs from 'fs';
import dayjs from 'dayjs';
@route('/upload')
class uploadController {
@route('/')
@POST()
async uploadFile(ctx) {
const { file } = ctx.request.files;
const reader = fs.createReadStream(file.path); // 文件内容
const path = `files/${dayjs().valueOf()}.zip`; // 文件路径(名称)
const data = await OssService.uploadFile(path, reader); // 调用service层方法
ctx.status = 200;
ctx.body = {
data,
code: 0,
msg: 'ok',
};
}
}
export default uploadController;
上传文件功能的使用场景也可以拓展至上传和下载中去,一个下载按钮的click事件绑定window.open('cdn地址')
,即可实现点击按钮下载文件功能。