文件上传原生js插件说明文档

文件上传原生js插件

前言

需求背景:在新疆项目中我们完成了视频上传的定制化开发,前端是基于第三方厂商(阳光云视)提供的代码进行二次封装,视频上传则是直接调用
阳光云视提供的接口。由于业务扩展现在由特定的项目开发转换为产品化开发,原来的视频上传插件不具备通用性无法移植到其他项目。

开发目标:

1.使用方便,通过配置基本配置项就可以达到上传的目的;
2.提供断点续传、文件秒传的功能;
3.可以并发上传;
4.接口调用从对应视频厂商改为调用自己后端接口(无论修改任何视频处理厂商都与前端无关);
5.无论系统采用哪种前端框架完成插件都可以适应。

1 使用

import upload from 'upLoad/upLoad.js';
...
this.SliceUploader = upload.init({
	autoUpload: true,
    id: 'selecter',
    onSuccess: this.onSuccess,
    onMD5Progress: this.onMD5Progress,
    onUploadProgress: this.onUploadProgress,
    onStartUpload: this.startUpload,
    onError: this.onError,
    onFileinQueue: this.onFileinQueue,
    maxSize: 600 * 1024 * 1024,
    extensions: 'mp4',
    chunkLimit: 1,
    chunkSize: 5 * 1024 * 1024,
    mimeType: 'video/mp4',
});

1.1 参数说明

参数名描述参数默认值
autoUpload为true时开启自动上传Booleantrue
id挂载事件的input标签idString-
maxSize上传文件最大大小Number300 * 1024 * 1024
extensions允许上传的文件类型,以任意字符隔开,为空时允许任意格式String-
chunkLimit分片上传请求并发数量Number2
chunkSize文件分片大小Number5 * 1024 * 1024
mimeType设置input元素的accept属性String-

1.2 事件说明

如果想阻塞,则在传入的方法中return false

事件参数说明描述
onFileinQueue*file File对象,对象包含一个uniqueValue参数,是插件类实例的唯一编码文件进入队列时触发
onStartUpload*file File对象当文件开始上传时触发
onSuccess*file File对象,对象包含一个successRes参数,是接口返回信息当前上传文件所有分片上传完成时触发
onMD5Progress*file File对象,对象包含一个MD5Progress参数,表示MD5的进度值MD5加密进度改变时触发
onUploadProgress*file File对象,对象包含一个percentage参数,表示上传进度值文件上传进度改变时触发
onError*Object {data:具体错误信息,code:错误代码}code=1:验证文件是否已上传接口报错 ;code=2:上传文件分片接口报错 ;code=3:手动调用doUpload方法时没有将参数autoUpload设为false;code=4:有文件分片上传失败; code=5:文件过大;code=6:文件类型错误;code=7:找不到相应id的DOM; code=8:网络错误

1.3 方法说明

方法参数说明描述
doUpload-手动触发文件上传,通常和autoUpload :false一起使用
handlePause-手动暂停文件上传
handleResume-手动恢复文件上传

1.4 upLoad.js的封装

为了隔离和保护upLoad.js中的变量和方法不被污染,整个文件中的代码包含在一个自执行函数中。
; (function (global) {......})(this);

最前面的分号是为了避免压缩或打包时变为函数。

upLoad的主要任务是执行文件的分片、验证是否已上传、执行上传等一系列操作。
为了完成这些任务,我们定义了一个SliceUpload并向外暴露。

//兼容CommonJs规范
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = SliceUpload;
  }
  //兼容AMD/CMD规范
  else if (typeof define === 'function') define(function () {
    return SliceUpload;
  })
  //注册全局变量,兼容直接使用script标签引入插件
  else global.SliceUpload = SliceUpload;
1.4.1 变量:

一些可以配置的变量或者是能够被获取及修改的变量,都定义在了:

function SliceUpload() {
    this.container = {
      file: null,
      hash: ""
    };
    this.hashPercentage = 0;
    this.percentage = 0;
    this.notUploadedIndex = null;
    this.data = []; // 上传的数据
    this.requestList = []; // xhr数组,传输失败,断点续传
    this.chunkLimit = 4;
    this.params = {
      userId: idStorage.fetch() || "321dsa122121",
      tenantId: tenantIdStorage.fetch() || "5",
      md5: "",
      uid: ""
    };
    this.md5Obj = {
      md5: "",
      percent: 0
    };
    this.chunkSize = 5 * 1024 * 1024;
    this.uploadURL = './transfer/aav/video/multipartUpload'
    this.checkURL = './transfer/aav/video/multipartUploadCheck'
    this.notUploadNumber = 0;
  };
1.4.2 回调:

我们在SliceUpload的原型链上定义了所有的回调,并在文件处理或者上传走到既定流程时,调用相应回调。

SliceUpload.prototype = {
    onFileinQueue() {/*文件进入队列 */ },
    onStartUpload() {/*开始上传 */ },
    onUploadProgress() { /*上传进度条改变 */ },
    onMD5Progress() { /*MD5加密进度条改变 */ },
    onError() {/*报错 */ },
    onSuccess() {/*上传完成 */ },
}
...
//执行md5加密和上传前执行onFileinQueue。
if(this.onFileinQueue(this.container.file)===false) return;
1.4.3公有方法:

与回调一样,我们在SliceUpload原型链上定义了一些公有方法,能够通过实例直接调用到。
包括:文件选择事件:handleFileChange、暂停:handlePause、续传/秒传判断:handleResume、手动执行上传:doUpload。

1.4.4 私有方法:

一些我们不希望被外部调用的方法,则定义在了SliceUpload的外部,调用时用call的方式重定向this。

1.5 index.js的封装

index.js的主要任务是创建SliceUpload实例、挂载事件,
为了完成这些任务,我们在其中定义并向外暴露了一个trsUpload对象,挂载都没

init方法

trsUpload 有一个init方法,接收配置项,用这些配置项生成一个SliceUpload实例并返回。

trsUpload.init = function (opt) {
        let example = new upLoad()
        Object.assign(example, defaultOPT, opt);
        eventInt(example);
        return example
    }

其中的eventInt是挂载input点击事件的方法。

2 vue环境之下使用方法

2.1 引入插件初始化

2.1.1 引入文件
import upload from '@/plugins/upLoad/index';
2.1.2 初始化实例
 mounted() {
        const opt = {
            autoUpload: true,//是否自动上传
            id: 'selecter',//绑定的dom元素ID
            onSuccess: this.onSuccess,//上传成功的回调方法
            onMD5Progress: this.onMD5Progress,//处理MD5进度条过程回调
            onUploadProgress: this.onUploadProgress,//上传文件进度条回调
            onStartUpload: this.startUpload,//自定义开始上传方法
            onError: this.onError,//错误处理回调
            onFileinQueue: this.onFileinQueue,//MD5开始之前文件进入队列回调
            maxSize: 600 * 1024 * 1024,//文件上传最大限制
            extensions: 'mp4',//文件上传后缀
            chunkLimit: 1,//上传并发数
            chunkSize: 5 * 1024 * 1024,//文件切片(分片)大小
            mimeType: 'video/mp4',//息内容类型的因特网标准
        };
        this.$nextTick(() => {
            this.SliceUploader = upload.init(opt);
        });
    },
注意:

以下代码:

   this.$nextTick(() => {
       this.SliceUploader = upload.init(opt);
   });

使用时指定的dom必须为input元素并且type=‘file’,如果需要用其他元素当做上传按钮,可隐藏input达到效果
在vue的环境下,因为是通过id选择器获取的dom所以要在页面初始化加载完毕之后再进行操作。

在插件的index.js中,init做了哪些事:

总体流程:
① init方法用opt新建并返回了一个upload的实例。
② 为input元素设置了属性,挂载了点击事件。触发点击事件后最终会走到upload的实例的handleFileChange方法。

在这里插入图片描述

(1)在index.js里面的init方法:

代码:

upLoad.init = function (opt) {
    let example = new upLoad()
    Object.assign(example, defaultOPT, opt);
    eventInt(example);
    return example
}

① 用传入的配置项创建了一个名为example的upLoad实例;
② 执行eventInt(),为指定的dom绑定事件。
③ 返回example。

(2)eventInt方法的定义同样在index.js中
let eventInt = function (example) {
    let dom = document.getElementById(example.id);
    if (dom) {
        dom.setAttribute('accept',example.mimeType)
        dom.addEventListener("change", function (e) {
            const [file] = e.target.files;
            if (file) {
                if ( checkSize(example, file.size) && checkExtensions(example, file.type)) example.handleFileChange(e)
            }
        });
    } else {
        example.onError({ data: 'The id is not found to be an DOM of ' + example.id, code: 7 })
    }
}
2.1.3 用户代码中绑定的回调事件示例
  methods: {
        onSuccess(res) {
            console.log(res);
        },
        onUploadProgress(file) {
            this.uploadProgress = file.percentage;
        },
        onMD5Progress(file) {
            this.md5Progress = file.MD5Progress;
        },
        onError(err) {
            console.log(err);
        },
        startUpload(file) {
            console.log(file);
        },
        onFileinQueue(file) {
            console.log(file);
            // return false;
        },
    },

3 设计流程

总体流程

总体流程思路
如流程图所示:

3.1 文件信息校验

这一块的逻辑处理实在index.js文件之中:

主要包括校验文件的基本信息,将本地选择的文件与用户定义的参数要求作比较,通过就可以执行下一步,不通过则回调错误信息。


let checkSize = function (example, size) {
    if (size <= example.maxSize) {
        return true;
    }
    example.onError({
        data: {
            en: 'The file exceeds the maximum limit',
            ch: '文件大小超出了最大限制'
        },
        code: 5
    })
}
let checkExtensions = function (example, type) {
    if ((example.extensions.includes(type.split('/').slice(-1)) && type) || example.extensions === '') {
        return true;
    }
    example.onError({
        data: {
            en: 'File type mismatch',
            ch: '文件类型不匹配'
        },
        code: 6
    })
}
3.2 用户上传的模式可以分为自动上传与用户点击上传按钮手动触发,两种上传的方式,通过用户初始化实例设置 autoUpload的值来区别:
autoUpload效果
true选择文件之后自动调用上传文件的方法
false调用者需要自己定义一个按钮作为触发文件上传的开关

调用者在初始化实例时需要规定上传是哪一种模式

代码示例:

<template>
    <div class="main">
        <input id="selecter" type="file" />
        <el-progress :percentage="md5Progress"></el-progress>
        <el-progress :percentage="uploadProgress"></el-progress>
        <input id="selecter1" type="file" />
        <el-button @click="SliceUploader1.doUpload()">上传</el-button>
        <el-progress :percentage="md5Progress1"></el-progress>
        <el-progress :percentage="uploadProgress1"></el-progress>
    </div>
</template>
<script>
import upload from '@/plugins/upLoad/index';
export default {
    name: 'Hello',
    data() {
        return {
            SliceUploader: {},
            SliceUploader1: {},
            md5Progress: 0,
            uploadProgress: 0,
            md5Progress1: 0,
            uploadProgress1: 0,
        };
    },
    computed: {
        state() {
            return this.SliceUploader.UploadState;
        },
    },
    mounted() {
        const opt = {
            autoUpload: true, // 是否自动上传
            id: 'selecter', // 绑定的dom元素ID
            onSuccess: this.onSuccess, // 上传成功的回调方法
            onMD5Progress: this.onMD5Progress, // 处理MD5进度条过程回调
            onUploadProgress: this.onUploadProgress, // 上传文件进度条回调
            onStartUpload: this.startUpload, // 自定义开始上传方法
            onError: this.onError, // 错误处理回调
            onFileinQueue: this.onFileinQueue, // MD5开始之前文件进入队列回调
            maxSize: 600 * 1024 * 1024, // 文件上传最大限制
            extensions: 'mp4', // 文件上传后缀
            chunkLimit: 1, // 上传并发数
            chunkSize: 5 * 1024 * 1024, // 文件切片(分片)大小
            mimeType: 'video/mp4', // 息内容类型的因特网标准
        };
        const opt1 = {
            autoUpload: false,
            id: 'selecter1',
            onSuccess: this.onSuccess1,
            onMD5Progress: this.onMD5Progress1,
            onUploadProgress: this.onUploadProgress1,
            onStartUpload: this.startUpload1,
            onError: this.onError1,
            onFileinQueue: this.onFileinQueue1,
            maxSize: 600 * 1024 * 1024,
            extensions: 'mp4',
            chunkLimit: 1,
            chunkSize: 5 * 1024 * 1024,
            mimeType: 'video/mp4',
        };
        this.$nextTick(() => {
            this.SliceUploader = upload.init(opt);
            this.SliceUploader1 = upload.init(opt1);
        });
    },
    methods: {
        onSuccess(res) {
            console.log(res);
        },
        onUploadProgress(file) {
            this.uploadProgress = file.percentage;
        },
        onMD5Progress(file) {
            this.md5Progress = file.MD5Progress;
        },
        onError(err) {
            console.log(err);
        },
        startUpload(file) {
            console.log(file);
        },
        onFileinQueue(file) {
            console.log(file);
            // return false;
        },
        onSuccess1(res) {
            console.log(res);
        },
        onUploadProgress1(file) {
            this.uploadProgress1 = file.percentage;
        },
        onMD5Progress1(file) {
            this.md5Progress1 = file.MD5Progress;
        },
        onError1(err) {
            console.log(err);
        },
        startUpload1(file) {
            console.log(file);
        },
        onFileinQueue1(file) {
            console.log(file);
        },
        uploadFileFun() {
            this.SliceUploader1.doUpload();
        },
    },
};
</script>
<style lang="less" scoped>
.main {
    text-align: center;
}
.fz100 {
    font-size: 100px;
}
</style>

效果如下:
是否自动上传文件示例图

3.3 文件上传校验

verifyUpload方法判断本地选择的文件是否再远端已经存在

状态执行
全部已经上传实现秒传
部分已经上传未上传的切片就绪,准备上传
全部未上传准备上传所有切片
3.4 上传
3.4.1 handleResume

引入promiseLimit,限制并发的数量,notuploadindexsArr存入没有上传的切片,调用uploadChunks方法实现切片上传:

  • 初始化limit实例,this.chunkLimit是指系统限制的个数,默认为4,用户也可以自定义
  • 首先采用verifyUpload判断本地选择的文件是否再远端已经存在
  • 如果已经存在的话,带给用户“秒传”的效果
  • 不存在的情况下引入promiseLimit,限制并发的数量,将notuploadindexsArr存入没有上传的切片,调用uploadChunks方法实现切片上传
3.4.2 uploadChunks
async function uploadChunks(notuploadArr = this.data) {
    const requestList = notuploadArr
      .map(({ file, index }) => {
        const formData = new FormData();
        formData.append("chunks", this.data.length);
        formData.append("file", file, this.container.file.name);
        formData.append("currentChunk", index);
        formData.append("chunkSize", file.size);
        Object.keys(this.params).forEach(key => {
          formData.append(key, this.params[key]);
        });
        return { formData, index };
      })
      .map(({ formData, index }) => {
        return doUploadChunk.call(this, formData, index);
      });
    Promise.all(requestList).then((res) => {
      if (this.notUploadNumber === 0) {
        uploadByTempPath.call(this, JSON.parse(res[res.length - 1].data).datas.fullname)
      } else {
        this.onError({ data: 'Some files failed to upload in pieces', code: 4 })
      }
      //所有上传完成时进入这里
    })
  };
  • 遍历切片列表,通过doUploadChunk创建发送上传切片请求的Promise实例,组成一个Promise实例列表requestList;
  • Promise.all将requestList中的多个Promise实例包装成一个新的Promise。
  • 所有请求完成后进入uploadByTempPath方法,后端进行切片合并,并触发onSuccess上传成功的回调
3.4.3 doUploadChunk
 function doUploadChunk(formData, index) {
    return limit(() => {
      return request.call(this, {
        url: this.uploadURL,
        method: "POST",
        data: formData,
        onProgress: createProgressHandler.call(this, this.data[index]),
        requestList: this.requestList // 重点
      });
    });
  };

① 根据limit的限制,实现多任务批量上传切片,limit是在handleResume中定义
② 调用request创建了调用上传切片的请求的promise实例

3.4.4 request 发送请求
function request({
    url,
    method = "post",
    data,
    headers = {},
    onProgress = e => e,
    requestList
  }) {
    return new Promise(resolve => {
      const xhr = new XMLHttpRequest();
      xhr.upload.onprogress = onProgress;
      xhr.open(method, url);
      Object.keys(headers).forEach(key => {
        xhr.setRequestHeader(key, headers[key]);
      });
      xhr.send(data);
      xhr.onload = e => {
        this.onUploadProgress(uploadPercentage.call(this));
        if (requestList) {
          // xhr 使命完成
          const xhrIndex = requestList.findIndex(item => {
            return item === xhr;
          });
          requestList.splice(xhrIndex, 1);
          if (e.target.status === 200) this.notUploadNumber--;
        }
        if (e.target.status !== 200 && !e.target.responseURL.includes(this.checkURL)) {
          this.onError({ data: e.target.response, code: 2 });
        }
        resolve({
          data: e.target.response
        });
      };
      if (requestList) {
        requestList.push(xhr); // 每个请求
      }
    });
  };

① 创建并返回一个Promise实例。
② 根据传入的参数,创建xhr实例,并将其压入requestList
③ 为xhr.upload.onprogress赋值onProgress,用于创建进度条,以及触发上传进度条值改变时的回调。
④ 请求完成时触发xhr.onload。

3.4.5 成功检验上传

调用接口,如果返回信息显示上传完毕,进度条的展示加载到100(前一步上传完切片最大展示为99%),并且回调onSuccess。

  async function uploadByTempPath(fullname) {
    const formData = new FormData();
    let arr = fullname.split('/')
    arr = arr[arr.length - 1].split('.')
    arr.pop();
    let fileName = arr.join('')
    formData.append("tempPath", fullname);
    formData.append("fileName", fileName);
    formData.append("mrEnable", 0);
    let msg = await request.call(this, {
      url: './bigdata/mr/personal/2/uploadByTempPath?',
      method: "POST",
      data: formData,
      headers: {
        "token": TOKEN.fetch() || ''
      }
    });
    setDatas.call(this, 'percentage', 100)
    this.container.file.successRes = msg
    if(this.onSuccess(this.container.file)===false) return;
  };

4 关键问题

4.1 如何获取进度条数据

(1)MD5处理的进度获取
  • 获取MD5值我们使用的是spark-md5资源,通过传入的文件参数以及切片大小的值,遍历文件,最后获得文件的MD5值,这样没遍历一次就会有一条“伪进度”作为回调数值返回给调用者。
  • MD5获取完毕之后,赋值到变量,以便后续文件上传接口参数的使用,此时回调进度之为100%
(2)文件上传的进度获取

在拿到 xhr.onload的回调信息之后,执行onUploadProgress回调方法,将进度信息回执给调用者。
给xhr.upload.onprogress经过了onProgress绑定createProgressHandler追踪事件

  • 绑定xhr.upload.onprogress
    在这里插入图片描述
    在这里插入图片描述
  • 得到上传进度的数值方法:
  function createProgressHandler(item) {
    return e => {
      if(this.onUploadProgress(uploadPercentage.call(this))===false) return;
      item.percentage = parseInt(String((e.loaded / e.total) * 100));
    };
  };

4.2 如何实现文件切片

  function createFileChunk(file, size = this.chunkSize) {
    const fileChunkList = [];
    let cur = 0;
    while (cur < file.size) {
      fileChunkList.push({ file: file.slice(cur, cur + size) });
      cur += size;
    }
    return fileChunkList;
  };

4.3 如何实现文件切片上传的并发数量限制

这里为了解决文件体积过大的情况下,切片数量过多,会出现并发数目过多而导致浏览器卡顿、甚至浏览器崩溃的情况;所以我们采用了promise-limit依赖包方法来限制并发数量,实现的效果:

  • 并发请求限制,n个请求,每次最多只能同时存在limit个请求,剩余的在队列中等待。
  • promiseAll 实现并发请求,n个请求,每次最多同时请求limit个,所有请求完成后处理数据。
引入promise-limit
  let promiseLimit = require('promise-limit')
初始化定义一个limit变量
var limit = promiseLimit(this.chunkLimit);

ps: chunkLimit是我们在初始化示例的时候定义的。

在执行每一步切片上传的时候
  function doUploadChunk(formData, index) {
    return limit(() => {
      return request.call(this, {
        url: this.uploadURL,
        method: "POST",
        data: formData,
        onProgress: createProgressHandler.call(this, this.data[index]),
        requestList: this.requestList // 重点
      });
    });
  };
附:

关于更多的promise-limit相关文档参考:

https://www.npmjs.com/package/promise-limit

4.4 判断文件是否已经上传以及秒传的实现

  • 首先采用verifyUpload判断本地选择的文件是否再远端已经存在,如果已经存在的话,带给用户“秒传”的效果
  • 不存在的情况下引入promiseLimit,限制并发的数量,将notuploadindexsArr代表的没有上传调用uploadChunks实现切片上传

4.5 断点续传

  • 断点续传与秒传采用的就是同一个接口,通过接口获得还没有上传的文件切片的“序号”,然后将没有上传的文件切片置入就绪队列,开始上传剩下的切片。

4.6 MD5的作用

前面我们多次提及到MD5,其作用如下:

  • 文件分片上传需要一个唯一标识符来判断文件的标志,MD5刚好可以作为这一个参数。对于不同的文件,MD5在几乎都是不同的(不排除一些实在特殊的情况之下)
  • 文件上传完毕以后远端会重新将上传的切片拼接,拼接之后再次获得新的MD5值与我们传递的MD5值对比,若是一致的则说明上传正确。

5.效果预览

5.1 git地址(vue示例版本)

https://git.trscd.com.cn/cdtrs/dev/03_super_star/tianMuYun/fileuploadmodule

5.2 在线实装地址(angularjs版本)

目前已将石榴云产品版本的视频上传底层更换为这个插件,在线地址为:

https://sly.trscd.com.cn/xjmediaCloud/#/fusionvideo?iwoplatform=0&wzchannelid=2&wzsiteid=1&wzplatform=0&appchannelid=1&appsiteid=1&appplatform=0&newspapertype=2&paperid=4&issave=0

账号/密码请咨询石榴云开发人员

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值