目录
1. 前言
2. 关于 vue-simple-uploader
3. 基于 vue-simple-uploader 封装全局上传组件
4. 文件上传流程概览
5. 文件分片
6. MD5 的计算过程
7. 秒传及断点续传
7.1 对于前端来说
7.2 前端做分片检验:
checkChunkUploadedByResponse
8. 源码及后记
1. 前言
之前公司要在管理系统中做一个全局上传插件, 即切换各个页面的时候, 上传界面还在并且上传不会受到影响, 这在 vue 这种 spa 框架面前并不是什么难题. 然而后端大佬说我们要实现分片上传, 秒传以及断点续传的功能, 听起来头都大了.
很久之前我写了一篇 webuploader 的文章, 结果使用起来发现问题很多, 且官方团队不再维护这个插件了, 经过多天调研及踩雷, 最终决定基于 vue-simple-uploader 插件实现该功能, 在项目中使用起来无痛且稳定.
如果你只是想实现基本的 (非定制化的) 上传功能, 直接使用 vue-simple-uploader, 多读一下它的文档, 不需要更多的二次封装.
如果你只是想实现全局上传插件, 也可以参照一下我的实现.
如果你用到了分片上传, 秒传及断点续传这些复杂的功能, 恭喜你, 这篇文章的重点就在于此.
本文源码在此:
2. 关于 vue-simple-uploader
vue-simple-uploader 是基于 simple-uploader.JS 封装的 vue 上传插件. 它的优点包括且不限于以下几种:
支持文件, 多文件, 文件夹上传; 支持拖拽文件, 文件夹上传
可暂停, 继续上传
错误处理
支持 "秒传", 通过文件判断服务端是否已存在从而实现 "秒传"
分块上传
支持进度, 预估剩余时间, 出错自动重试, 重传等操作
读这篇文章之前, 建议先读一遍 simple-uploader.JS 的文档, 然后再读一下 vue-simple-uploader 的文档, 了解一下各个参数的作用是什么, 我在这里假定大家已经比较熟悉了..
vue-simple-uploader 文档
simple-uploader.JS 文档
安装: NPM install vue-simple-uploader --save
使用: 在 main.JS 中:
- import uploader from 'vue-simple-uploader'
- Vue.use(uploader)
3. 基于 vue-simple-uploader 封装全局上传组件
引入 vue-simple-uploader 后, 我们开始封装全局的上传组件 globalUploader.vue, 代码比较长, 就不整个放出来了, 源码放到 GitHub 上了, 这里一步一步地讲解.
template 部分如下, 本人自定义了模板和样式, 所以 html 部分比较长, CSS 部分暂时不列出, 大家可以根据自己的 ui 去更改, 主要关注一下 uploader 这个组件的 options 参数及文件 added,success,progress,error 几个事件:
- <template>
- <div id="global-uploader">
- <!-- 上传 -->
- <uploader
- ref="uploader"
- :options="options"
- :autoStart="false"
- @file-added="onFileAdded"
- @file-success="onFileSuccess"
- @file-progress="onFileProgress"
- @file-error="onFileError"
- class="uploader-app">
- <uploader-unsupport></uploader-unsupport>
- <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
- <uploader-list v-show="panelShow">
- <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
- <div class="file-title">
- <h2 > 文件列表</h2>
- <div class="operate">
- <el-button @click="fileListShow" type="text" :title="collapse ?'展开':'折叠' ">
- <i class="iconfont" :class="collapse ?'icon-fullscreen':'icon-minus-round'"></i>
- </el-button>
- <el-button @click="close" type="text" title="关闭">
- <i class="iconfont icon-close"></i>
- </el-button>
- </div>
- </div>
- <ul class="file-list">
- <li v-for="file in props.fileList" :key="file.id">
- <uploader-file :class="'file_' + file.id"ref="files":file="file":list="true"></uploader-file>
- </li>
- <div class="no-file" v-if="!props.fileList.length"><i class="nucfont inuc-empty-file"></i> 暂无待上传文件</div>
- </ul>
- </div>
- </uploader-list>
- </uploader>
- </div>
- </template>
组件中的 data 部分:
- data() {
- return {
- options: {
- target: 'http://xxxxx/xx', // 目标上传 URL
- chunkSize: '2048000', // 分块大小
- fileParameterName: 'file', // 上传文件时文件的参数名, 默认 file
- testChunks: true, // 是否开启秒传
- maxChunkRetries: 3, // 最大自动失败重试上传次数
- // 服务器分片校验, 断点续传基础
- checkChunkUploadedByResponse: function (chunk, message) {
- let objMessage = JSON.parse(message);
- if (objMessage.skipUpload) {
- return true;
- }
- return (objMessage.uploaded || []).indexOf(chunk.offset + 1)>= 0
- },
- headers: {
- // 在 header 中添加的验证, 请根据实际业务来
- Authorization: "Bearer" + Ticket.get().access_token
- },
- },
- attrs: {
- // 接受的文件类型, 形如['.png', '.jpg', '.jpeg', '.gif', '.bmp'...] 这里我封装了一下
- accept: ACCEPT_CONFIG.getAll()
- },
- panelShow: false, // 选择文件后, 展示上传 panel
- }
- },
全局引用:
在 App.vue 中引用, 即作为全局的组件一直存在, 只不过在不使用的时候把上传界面隐藏了
<global-uploader></global-uploader>
4. 文件上传流程概览
1. 点击按钮, 触发文件上传操作:
(如果你做的不是全局上传的功能, 而是直接点击上传, 忽略这一步.)
因为我做的是全局上传的插件, 要先把上传的窗口隐藏起来, 在点击某个上传按钮的时候, 用 Bus 发送一个 openUploader 的事件, 在 globalUploader.vue 中接收该事件, trigger 我们 uploader-btn 的 click 事件.
在某个页面中, 点击上传按钮, 同时把要给后台的参数带过来(如果有的话), 这里组件之间传值我用的 event bus, 当然用 vuex 会更好:
- Bus.$emit('openUploader', {
- superiorID: this.superiorID
- })
在 globalUploader.vue 中接收该事件:
- Bus.$on('openUploader', query => {
- this.params = query || {};
- if (this.$refs.uploadBtn) {
- // 这样就打开了选择文件的操作窗口
- $('#global-uploader-btn').click();
- }
- });
2. 选择文件后, 将上传的窗口展示出来, 开始 md5 的计算工作
- onFileAdded(file) {
- this.panelShow = true;
- // 计算 MD5, 下文会提到
- this.computeMD5(file);
- },
这里有个前提, 我在 uploader 中将 autoStart 设为了 false, 为什么要这么做?
在选择文件之后, 我要计算 MD5, 以此来实现断点续传及秒传的功能, 所以选择文件后直接开始上传肯定不行, 要等 MD5 计算完毕之后, 再开始文件上传的操作.
具体的 MD5 计算方法, 会在下面讲, 这里先简单引出.
上传过程中, 会不断触发 file-progress 上传进度的回调
- // 文件进度的回调
- onFileProgress(rootFile, file, chunk) {
- console.log(` 上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
- },
3. 文件上传成功后
文件上传成功后, 在 "上传完成" 的回调中, 通过服务端返回的 needMerge 字段, 来判断是否需要再发送合并分片的请求,
如果这个字段为 true, 则需要给后台发一个请求合并的 Ajax 请求, 否则直接上传成功.
注意: 这里的 needMerge 是我和后台商议决定的字段名
- onFileSuccess(rootFile, file, response, chunk) {
- let res = JSON.parse(response);
- // 服务器自定义的错误, 这种错误是 Uploader 无法拦截的
- if (!res.result) {
- this.$message({ message: res.message, type: 'error' });
- return
- }
- // 如果服务端返回需要合并
- if (res.needMerge) {
- API.mergeSimpleUpload({
- tempName: res.tempName,
- fileName: file.name,
- ...this.params,
- }).then(data => {
- // 文件合并成功
- Bus.$emit('fileSuccess', data);
- }).catch(e => {});
- // 不需要合并
- } else {
- Bus.$emit('fileSuccess', res);
- console.log('上传成功');
- }
- },
- onFileError(rootFile, file, response, chunk) {
- console.log(error)
- },
5. 文件分片
vue-simple-uploader 自动将文件进行分片, 在 options 的 chunkSize 中可以设置每个分片的大小.
如图: 对于大文件来说, 会发送多个请求, 下面的第一个请求是 get 请求, 用于和服务端分片校验, 后面的每一个请求都是上传分片的 post 请求
看一下发送给服务端的参数, 其中 chunkNumber 表示当前是第几个分片, totalChunks 代表所有的分片数, 这两个参数都是都是插件根据你设置的 chunkSize 来计算的.
需要注意的就是在最后文件上传成功的事件中, 通过后台返回的字段, 来判断是否要再给后台发送一个文件合并的请求.
6. MD5 的计算过程
断点续传及秒传的基础是要计算文件的 MD5, 这是文件的唯一标识, 然后服务器根据 MD5 进行判断, 是进行秒传还是断点续传.
在 file-added 事件之后, 就计算 MD5, 我们最终的目的是将计算出来的 MD5 加到参数里传给后台, 然后继续文件上传的操作, 详细的思路步骤是:
把 uploader 组件的 autoStart 设为 false, 即选择文件后不会自动开始上传
先通过 file.pause()暂停文件, 然后通过 H5 的 FileReader 接口读取文件
将异步读取文件的结果进行 MD5, 这里我用的加密工具是 spark-md5, 你可以通过
NPM install spark-md5 --save
来安装, 也可以使用其他 MD5 加密工具.
file 有个属性是 uniqueIdentifier, 代表文件唯一标示, 我们把计算出来的 MD5 赋值给这个属性
file.uniqueIdentifier = md5
, 这就实现了我们最终的目的.
通过 file.resume()开始 / 继续文件上传.
- /**
- * 计算 md5, 实现断点续传及秒传
- * @param file
- */
- computeMD5(file) {
- let fileReader = new FileReader();
- let time = new Date().getTime();
- let md5 = '';
- file.pause();
- fileReader.readAsArrayBuffer(file.file);
- fileReader.onload = (e => {
- if (file.size != e.target.result.byteLength) {
- this.error('Browser reported success but could not read the file until the end.');
- return
- }
- md5 = SparkMD5.ArrayBuffer.hash(e.target.result);
- // 添加额外的参数
- this.uploader.opts.query = {
- ...this.params
- }
- console.log(`MD5 计算完毕:${file.id} ${file.name} MD5:${md5} 用时:${new Date().getTime() - time} ms`);
- file.uniqueIdentifier = md5;
- file.resume();
- });
- fileReader.onerror = function () {
- this.error('FileReader onerror was triggered, maybe the browser aborted due to high memory usage.');
- };
- },
7. 秒传及断点续传
在计算完 MD5 后, 我们就能谈断点续传及秒传的概念了.
服务器根据前端传过来的 MD5 去判断是否可以进行秒传或断点续传:
a. 服务器发现文件已经完全上传成功, 则直接返回秒传的标识.
b. 服务器发现文件上传过分片信息, 则返回这些分片信息, 告诉前端继续上传, 即断点续传.
7.1 对于前端来说
在每次上传过程的最开始,
vue-simple-uploader
会发送一个 get 请求, 来问服务器我哪些分片已经上传过了,
这个请求返回的结果也有几种可能:
a. 如果是秒传, 在请求结果中会有相应的标识, 比如我这里是 skipUpload 为 true, 且返回了 url, 代表服务器告诉我们这个文件已经有了, 我直接把 url 给你, 你不用再传了, 这就是秒传.
图 a1: 秒传情况下后台返回值
图 a2: 秒传 gif
b. 如果后台返回了分片信息, 这是断点续传. 如图, 返回的数据中有个 uploaded 的字段, 代表这些分片是已经上传过的了, 插件会自动跳过这些分片的上传.
图 b1: 断点续传情况下后台返回值
图 b2: 断点续传 gif
c. 可能什么都不会返回, 那这就是个全新的文件了, 走完整的分片上传逻辑
7.2 前端做分片检验:
checkChunkUploadedByResponse
前面讲的是概念, 现在说一说前端在拿到这些返回值之后怎么处理.
插件自己是不会判断哪个需要跳过的, 在代码中由 options 中的 checkChunkUploadedByResponse 控制, 它会根据 XHR 响应内容检测每个块是否上传成功了, 成功的分片直接跳过上传
你要在这个函数中进行处理, 可以跳过的情况下返回 true 即可.
- checkChunkUploadedByResponse: function (chunk, message) {
- let objMessage = JSON.parse(message);
- if (objMessage.skipUpload) {
- return true;
- }
- return (objMessage.uploaded || []).indexOf(chunk.offset + 1)>= 0
- },
注: skipUpload 和 uploaded 是我和后台商议的字段, 你要按照后台实际返回的字段名来.
8. 源码及后记
总共几个文件, App.vue, 封装的全局上传组件 globalUploader.vue, 调用组件的 demo.vue, 源码放到 GitHub 上了:.
globalUploader 源码中的 ticket 和 API 都是自己用的, 一个是 accesstoken, 一个是基于 axios 封装的请求库, 请根据你的业务需求替代之. 另外上传界面的展开和收起用到了 jQuery, 通知用到了 Element 的组件, 请忽略之.
本人水平有限, 更多的是提供一个思路, 供大家参考.
封装完这个插件后, 再加上开发文件资源库, 我发现已经基本实现了一个简易的百度网盘了, 一个管理系统, 功能搞的这么复杂, 坑爹啊!
来源: https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html