前端Vue+后端Node.js,手撕一个文件上传功能

1,457 阅读3分钟

需求背景

    最近需要做一个文件上传的功能。前端使用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地址'),即可实现点击按钮下载文件功能。