Vue+elelemtUI实现断点续传(前端)

一、在config->index.js中设置 proxy

1 devServer: {
2         proxy: {
3             '/': {
4                 target: 'http://127.0.0.1:8888',
5                 changeOrigin: true
6             }
7         }
8     }

二、下载并引用相应依赖 (main.js)

  本次demo是利用 Element-ui 配合开发。

  所需依赖(我的版本):

    "element-ui": "^2.13.2",

    "axios": "^0.19.2",

    "spark-md5": "^3.0.1",

    "qs": "^6.9.4",

   如果嫌一个一个下载麻烦,可以把上面内容复制到 package.json 中 “dependencies” ,执行  "npm install" 即可。

  main.js引入所需包:

1 import ElementUI from 'element-ui';
2 import 'element-ui/lib/theme-chalk/index.css';
3 Vue.use(ElementUI);

  App.vue中引入所需包:

  

1 import { fileParse } from "./assets/utils";
2 import axios from "axios";
3 import SparkMD5 from "spark-md5";

三、HTML部分

  使用Element-UI实现前端样式展示

 1 <template>
 2   <div >
 3     <el-upload drag action :auto-upload="false" :show-file-list="false" :on-change="changeFile">
 4       <i class="el-icon-upload"></i>
 5       <div class="el-upload__text">
 6         将文件拖到此处,或
 7         <em>点击上传</em>
 8       </div>
 9     </el-upload>
10 
11     <!-- PROGRESS -->
12     <div class="progress">
13       <span>上传进度:{{total|totalText}}%</span>
14       <el-link type="primary" v-if="total>0 && total<100" @click="handleBtn">{{btn|btnText}}</el-link>
15     </div>
16 
17     <!-- VIDEO -->
18     <div class="uploadImg" v-if="video">
19       <video :src="video" controls />
20     </div>
21   </div>
22 </template>

四、使用 promise 封装 fileParse方法

    promise:promise构造函数是同步执行的,并且是立即执行的函数,promise.then中的函数是异步的。并且promise状态改变后将不会再更改。

    Promise有三个状态:pending(等待)、fulfilled(实现)、rejected(拒绝),其中resolvereject只有第一次执行有效。

  utils.js(对文件转义形式进行封装,根据所传入参数确定封装格式【base64、buffer】)

 1 export function fileParse(file, type = "base64") {
 2     return new Promise(resolve => {
 3         let fileRead = new FileReader();
 4         if (type === "base64") {
 5             fileRead.readAsDataURL(file);
 6         } else if (type === "buffer") {
 7             fileRead.readAsArrayBuffer(file);
 8         }
 9         fileRead.onload = (ev) => {
10             resolve(ev.target.result);
11         };
12     });
13 };

五、逻辑流程(前端)

   

  1、利用结合promise封装filepase方法,解析文件为buffer数据

  2、使用sparkMD5生成文件的哈希值,并获取后缀;使用spark.append(buffer)生成哈希值

  3、通过正则获取文件后缀

  4、创建切面默认参数,包括:切片个数、索引值和结束值

  5、通过flie.slice()进行切片

  6、进行遍历上传切片,判断当前索引  >=  切片数组后停止,调用合并文件接口告知服务器进行合并

  

六、项目整体代码

  1 <script>
  2 import { fileParse } from "./assets/utils";
  3 import axios from "axios";
  4 import SparkMD5 from "spark-md5";
  5 
  6 export default {
  7   name: "App",
  8   data() {
  9     return {
 10       total: 0,
 11       video: null,
 12       btn: false,
 13     };
 14   },
 15   filters: {
 16     btnText(btn) {
 17       return btn ? "继续" : "暂停";
 18     },
 19     totalText(total) {
 20       return total > 100 ? 100 : total;
 21     },
 22   },
 23   methods: {
 24     async changeFile(file) {
 25       if (!file) return;
 26       file = file.raw;
 27 
 28       // 解析为BUFFER数据
 29       // 我们会把文件切片处理:把一个文件分割成为好几个部分(固定数量/固定大小)
 30       // 每一个切片有自己的部分数据和自己的名字
 31       // HASH_1.mp4
 32       // HASH_2.mp4
 33       // ...
 34       let buffer = await fileParse(file, "buffer"),
 35         spark = new SparkMD5.ArrayBuffer(),
 36         hash,
 37         suffix;
 38       spark.append(buffer);
 39       hash = spark.end();
 40       suffix = /.([0-9a-zA-Z]+)$/i.exec(file.name)[1];
 41 
 42       // 创建100个切片
 43       let partList = [],
 44         partsize = file.size / 100,
 45         cur = 0;
 46       for (let i = 0; i < 100; i++) {
 47         let item = {
 48           chunk: file.slice(cur, cur + partsize),
 49           filename: `${hash}_${i}.${suffix}`,
 50         };
 51         cur += partsize;
 52         partList.push(item);
 53       }
 54 
 55       this.partList = partList;
 56       this.hash = hash;
 57       this.sendRequest();
 58     },
 59     async sendRequest() {
 60       // 根据100个切片创造100个请求(集合)
 61       let requestList = [];
 62       this.partList.forEach((item, index) => {
 63         // 每一个函数都是发送一个切片的请求
 64         let fn = () => {
 65           let formData = new FormData();
 66           formData.append("chunk", item.chunk);
 67           formData.append("filename", item.filename);
 68           return axios
 69             .post("/single3", formData, {
 70               headers: { "Content-Type": "multipart/form-data" },
 71             })
 72             .then((result) => {
 73               result = result.data;
 74               if (result.code == 0) {
 75                 this.total += 1;
 76                 // 传完的切片我们把它移除掉
 77                 this.partList.splice(index, 1);
 78               }
 79             });
 80         };
 81         requestList.push(fn);
 82       });
 83 
 84       // 传递:并行(ajax.abort())/串行(基于标志控制不发送)
 85       let i = 0;
 86       let complete = async () => {
 87         let result = await axios.get("/merge", {
 88           params: {
 89             hash: this.hash,
 90           },
 91         });
 92         result = result.data;
 93         if (result.code == 0) {
 94           this.video = result.path;
 95         }
 96       };
 97       let send = async () => {
 98         // 已经中断则不再上传
 99         if (this.abort) return;
100         if (i >= requestList.length) {
101           // 都传完了
102           complete();
103           return;
104         }
105         await requestList[i]();
106         i++;
107         send();
108       };
109       send();
110     },
111     handleBtn() {
112       if (this.btn) {
113         //断点续传
114         this.abort = false;
115         this.btn = false;
116         this.sendRequest();
117         return;
118       }
119       //暂停上传
120       this.btn = true;
121       this.abort = true;
122     },
123   },
124 };
125 </script>

  参考资料:https://www.bilibili.com/video/BV18v411v7Xu?t=346