11

公司最近需要使用阿里OSS上传文件,但是文件类型不固定,开始的想法是通过Java写接口,如果文件过大由后端对文件进行分片处理并上传。在调研过程中发现ali-oss公共模块中提供了分片上传的方法,所以找个工作就交给了前端来做,以减轻后端的压力,于是笔者就开始了漫长的调研过程。

初期考虑只是简单的实现就好,但是为了以后方便维护以及复用情况笔者考虑,使用class进行封装处理。

使用技术栈:

  1. vue-cli 3.0
  2. typescript

依赖:

  1. ali-oss
  2. Element-ui(可忽略)

测试用html结构如下(使用了element-ui)

<div class="home">
    <input type="file" multiple='true' @change="onFileChange" id="file">
    <el-button @click="upload">普通上传</el-button>
    <el-button @click="multipartUpload">分片上传</el-button>
    <el-button @click="stop">停止上传</el-button>
    <el-button @click="resume">中断续传</el-button>
    <el-progress :percentage="percentage"></el-progress>
</div>

前端直接对接阿里oss需要使用ali-oss公共包,执行一下命令

npm install --save-dev ali-oss

创建文件DockingOSS.ts

class DockingOSS {

}

由于依赖于ali-oss,要考虑到ali-oss在初始化时所需要的参数,封装类时所需要的参数,由于使用typescript就不能再让参数随意填写,而是使用接口对参数进行规范化处理。

interface allOssInterface {
    region:string;  //  地域节点,必填
    accessKeyId:string; //  用户id,必填
    accessKeySecret:string; //  访问密钥,必填
    bucket:string;  //  bucket名称,qjdev-pred-voices
    path?:string;   //  路径,默认为"",用户长传到指定文件夹
    secure?:Boolean;    //  指示OSS客户端使用 HTTPS:true HTTP:false
    parallel?:number;   //  分片数量
    partSize?:number;   //  分片大小
    defaultName?:Boolean;   //  是否使用默认名称
    length?:number; //  随机名称长度
};

对其进行初始化

class DockingOSS {

    //  ali-oss 实例
    private allOSS:any;
    private parallel:number;
    private partSize:number;
    private defaultName:Boolean;
    private path:string;
    private length:number;

    constructor(data:allOssInterface){
        let {region,
            accessKeyId,
            accessKeySecret,
            bucket,
            secure = true,
            parallel = 3,
            partSize = 1024 * 1024,
            defaultName = false,
            path = "",
            length = 50} = data;
        this.partSize = partSize;
        this.parallel = parallel;
        this.defaultName = defaultName;
        this.path = path;
        this.length = length;
        //  实例化ali-oss
        this.allOSS = new AliOss({region,accessKeyId,accessKeySecret,bucket,secure});
    }
    
}

添加普通上传方法,处于考虑到开发者可能需要把文件上传到不同的文件夹,以及会使用随机文件名称或者使用固定文件名称,定义了两个方法用来处理上传路径和文件名称。

class DockingOSS { 

    /**
     * 普通上传
     * file:文件对象
     * _fileName:固定文件名称
     */
    public upload(file:File,_fileName:string = ""):Promise<any>{
        let fileName = _fileName;
        (!fileName) && (fileName = this.getFileName(file));
        const pathName = this.accessPath(fileName);
        return this.allOSS.put(pathName, file)
    }
    
}

添加获取路径和随机名称方法

class DockingOSS { 

    //  获取路径
    private accessPath(fileName:string):string {
        const {path} = this;
        return path?`${path}/${fileName}`:fileName;
    }

    //  获取名称
    private getFileName(file:File):string{
        let {defaultName} = this;
        const fileName:string = file.name;
        if(defaultName) return fileName;
        return this.randomFileName();
    }

    //  随机文件名称
    private randomFileName():string{
        const data = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F",
                        "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
                        "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r",
                        "s", "t", "u", "v", "w", "x", "y", "z"];
        let nums = "";
        const {length} = this;
        for (let i = 0; i < length; i++) {
            const randomStr:string = (Math.random()*61).toString()
            const r:number = parseInt(randomStr, 10);
            nums += (data[r]).toString();
        }
        return nums;
    }
    
}

添加分片上传上传方法

class DockingOSS { 

    /**
     * 分片上传
     * file:文件对象
     * _fileName:固定文件名称
     * progress:分片上传进度回调函数
     */
    public multipartUpload(file:File, progress:Function,_fileName:string):Promise<void>{
        const {parallel,partSize} = this;
        let fileName = _fileName;
        (!fileName) && (fileName = this.getFileName(file));
        const pathName = this.accessPath(fileName);
        return this.allOSS.multipartUpload(pathName, file, {
            parallel,
            partSize,
            progress
        })
    }
    
}

添加中止上传方法

class DockingOSS { 

    //  中止上传
    public cancel():void{
        this.allOSS.cancel();
    }

}

添加续传方法,由于在续传时需要接收一些参数,需要从中获取到中止上传的文件对象,使用interface对参数进行规范化。

interface checkpointInterface {
    file:File;
    name:string;
    fileSize:number;
    partSize:number;
    uploadId:string;
};

class DockingOSS { 

    /**
     * 分片续传
     * checkpoint:中断上传的文件
     * progress:进度回调函数
     */
    public resume(checkpoint:checkpointInterface, progress:Function):Promise<void>{
        const { uploadId, file } = checkpoint;
        const {parallel,partSize,path} = this;
        return this.allOSS.multipartUpload(uploadId, file, {
            parallel,
            partSize,
            progress,
            checkpoint
        })
    }

}

简易封装就完成了,虽然封装不算太完善但是还是可以满足大部分项目需求的,在应用过程中,可能会很多地方用到该类,可以在类中添加单例,已保证整个项目中只存在一个实例,减少对内存的占用(提高性能从小事开始做起)。

实战应用:

<template>
  <div class="home">
    <input type="file" multiple='true' @change="onFileChange" id="file">
    <el-button @click="upload">普通上传</el-button>
    <el-button @click="multipartUpload">分片上传</el-button>
    <el-button @click="stop">停止上传</el-button>
    <el-button @click="resume">中断续传</el-button>
    <el-progress :percentage="percentage"></el-progress>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import AliOss from "ali-oss";

import DockingOSS from '@/assets/DockingOSS';

let dockOss = new DockingOSS({  
  //  地域节点
  region:"*********",
  //  id
  accessKeyId:"************",
  //  访问密钥
  accessKeySecret:"***********",
  //  bucket
  bucket:"***********",
  path:"***********",
  secure: true
})

@Component
export default class HelloWorld extends Vue {
  //    中断上传存储文件内容
  private checkpoints:object = {};
  private percentage:number = 0;
  private filesArr:File[] = [];

  //  文件更改
  private onFileChange(e:Event):void{
    const target = Reflect.get(e,"target");
    const value = Reflect.get(target,"value");
    const tmpcnt = value.lastIndexOf(".")
    const exname = value.substring(tmpcnt + 1)
    const oFile = document.getElementById("file") || document.createElement("input");
    const files:File[] = Reflect.get(oFile,"files");
    const fileList = Array.from(files);
    this.filesArr = fileList;
  }

  //  分片上传
  private multipartUpload() {
    this.percentage = 0;
    this.filesArr.forEach((file:File) => {
        //  当停止上传时需要也会走向catch
        //  错误提示:{status: 0, name: "cancel"}
        //  需要特殊处理
        dockOss.multipartUpload(file,this.onMultipartUploadProgress)
    });
  }

  //  上传进度
  //  checkpoint:返回的文件对象
  //  如果文件太小,小于分片大小的话,则不会checkpoint,为undefined
  //  说明文件直接上传成功了
  private onMultipartUploadProgress(e:number,checkpoint:any){
    Reflect.set(this.checkpoints,checkpoint.uploadId,checkpoint)
    this.percentage = Number((e * 100).toFixed(0));
    if(e === 1){
      Reflect.deleteProperty(this.checkpoints,checkpoint.uploadId);
    }
  }

  //  普通上传
  private upload():void{
    this.filesArr.forEach((file:File) => {
      dockOss.upload(file);
    })
  }

  //  中止上传
  private stop():void{
    dockOss.cancel()
  }

  //  续传
  private resume():void{
    Object.values(this.checkpoints).forEach((checkpoint) => {
      dockOss.resume(checkpoint,this.onMultipartUploadProgress)
    });
  }

}
</script>

对接阿里oss也是没那么困难的,还是想记录一下,毕竟不用每次都需要翻阅资料了。

需要说明的一点,为什么没有直接使用element-ui的上传组件,为了节省oss空间,以及防止用户误传文件,所以使用这种方式在用户提交数据时,进行文件上传,这样会更好一些。

记录生活分享技术,共同进步共同成长。如果文章中由什么错误,请在评论出提出指正,我会及时做出修改。


Aaron
4k 声望6.1k 粉丝

Easy life, happy elimination of bugs.