JS切片上传
Dddusty 人气:0前言
本篇介绍了切片上传的基本实现方式(前端),以及实现切片上传后的一些附加功能,切片上传原理较为简单,代码注释比较清晰就不多赘述了,后面的附加功能介绍了实现原理,并贴出了在原本代码上的改进方式。有什么错误希望大佬可以指出,感激不尽。
切片后上传
切片上传的原理较为简单,即获取文件后切片,切片后整理好每个切片的参数并发请求即可。
下面直接上代码:
HTML
<template> <div> <input type="file" @change="handleFileChange" /> <el-button @click="handleUpload">上传</el-button> </div> </template>
JavaScript
<script> const SIZE = 10 * 1024 * 1024; // 切片大小 export default { data: () => ({ // 存放文件信息 container: { file: null hash: null }, data: [] // 用于存放加工好的文件切片列表 hashPercentage: 0 // 存放hash生成进度 }), methods: { // 获取上传文件 handleFileChange(e) { const [file] = e.target.files; if (!file) { this.container.file = null; return; } this.container.file = file; }, // 生成文件切片 createFileChunk(file, size = SIZE) { const fileChunkList = []; let cur = 0; while (cur < file.size) { fileChunkList.push({ file: file.slice(cur, cur + size) }); cur += size; } return fileChunkList; }, // 生成文件hash calculateHash(fileChunkList) { return new Promise(resolve => { this.container.worker = new Worker("/hash.js"); this.container.worker.postMessage({ fileChunkList }); this.container.worker.onmessage = e => { const { percentage, hash } = e.data; // 可以用来显示进度条 this.hashPercentage = percentage; if (hash) { resolve(hash); } }; }); }, // 切片加工(上传前预处理 为文件添加hash等) async handleUpload() { if (!this.container.file) return; // 切片生成 const fileChunkList = this.createFileChunk(this.container.file); // hash生成 this.container.hash = await this.calculateHash(fileChunkList); this.data = fileChunkList.map(({ file },index) => ({ chunk: file, // 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名 hash: this.container.hash + "-" + index })); await this.uploadChunks(); } // 上传切片 async uploadChunks() { const requestList = this.data // 构造formData .map(({ chunk,hash }) => { const formData = new FormData(); formData.append("chunk", chunk); formData.append("hash", hash); formData.append("filename", this.container.file.name); return { formData }; }) // 发送请求 上传切片 .map(async ({ formData }) => request(formData) ); await Promise.all(requestList); // 等待全部切片上传完毕 await merge(this.container.file.name) // 发送请求合并文件 }, } }; </script>
生成hash
无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则
这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互
由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts`函数用于导入外部脚本,通过它导入 spark-md5
// /public/hash.js self.importScripts("/spark-md5.min.js"); // 导入脚本 // 生成文件 hash self.onmessage = e => { const { fileChunkList } = e.data; const spark = new self.SparkMD5.ArrayBuffer(); let percentage = 0; let count = 0; const loadNext = index => { // 新建读取器 const reader = new FileReader(); // 设定读取数据格式并开始读取 reader.readAsArrayBuffer(fileChunkList[index].file); // 监听读取完成 reader.onload = e => { count++; // 获取读取结果并交给spark计算hash spark.append(e.target.result); if (count === fileChunkList.length) { self.postMessage({ percentage: 100, // 获取最终hash hash: spark.end() }); self.close(); } else { percentage += 100 / fileChunkList.length; self.postMessage({ percentage }); // 递归计算下一个切片 loadNext(count); } }; }; loadNext(0); };
小结
- 获取上传文件
- 文件切片后存入数组 fileChunkList.push({ file: file.slice(cur, cur + size) });
- 生成文件hash(非必须)
- 根据文件切片列表生成请求列表
- 并发请求
- 待全部请求完成后发送合并请求
文件秒传
实际是障眼法,用来欺骗用户的。
原理:在文件上传之前先计算出文件的hash,然后发送给后端进行验证,看后端是否存在这个hash,如果存在,则证明这个文件上传过,则直接提示用户秒传成功
// 切片加工(上传前预处理 为文件添加hash等) async handleUpload() { if (!this.container.file) return; // 切片生成 const fileChunkList = this.createFileChunk(this.container.file); // hash生成 this.container.hash = await this.calculateHash(fileChunkList); // hash验证 (verify为后端验证接口请求) const { haveExisetd } = await verify(this.container.hash) // 判断 if(haveExisetd) { this.$message.success("秒传:上传成功") return } this.data = fileChunkList.map(({ file },index) => ({ chunk: file, // 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名 hash: this.container.hash + "-" + index })); await this.uploadChunks(); }
暂停上传
原理:将所有的切片存在一个数组中,每当一个切片上传完毕,从数组中移除,这样就可以实现用一个数组只保存上传中的文件。此外,因为要暂停上传,所以需要中断请求 axios
中断请求可以利用AbortController
中断请求示例
const controller = new AbortController() axios({ signal: controller.signal }).then(() => {}); // 取消请求 controller.abort()
添加暂停上传功能
// 上传切片 async uploadChunks() { // 需要把requestList放到全局,因为要通过操控requestList来实现中断 this.requestList = this.data // 构造formData .map(({ chunk,hash }) => { const formData = new FormData(); formData.append("chunk", chunk); formData.append("hash", hash); formData.append("filename", this.container.file.name); return { formData }; }) // 发送请求 上传切片 .map(async ({ formData }, index) => request(formData).then(() => { // 将请求成功的请求剥离出requestList this.requestList.splice(index, 1) }) ); await Promise.all(this.requestList); // 等待全部切片上传完毕 await merge(this.container.file.name) // 发送请求合并文件 }, // 暂停上传 handlePause() { this.requestList.forEach((req) => { // 为每个请求新建一个AbortController实例 const controller = new AbortController(); req.signal = controller.signal controller.abort() }) }
恢复上传
原理:上传切片之前,向后台发送请求,接口将已上传的切片列表返回,通过切片hash将后台已存在的切片过滤,只上传未存在的切片
// 切片加工(上传前预处理 为文件添加hash等) async handleUpload() { if (!this.container.file) return; // 切片生成 const fileChunkList = this.createFileChunk(this.container.file); // 文件hash生成 this.container.hash = await this.calculateHash(fileChunkList); // hash验证 (verify为后端验证接口请求) const { haveExisetd, uploadedList } = await verify(this.container.hash) // 判断 if(haveExisetd) { this.$message.success("秒传:上传成功") return } this.data = fileChunkList.map(({ file },index) => ({ chunk: file, // 注:这个是切片hash 这里的hash为文件名 + 切片序号,也可以用md5对文件进行加密获取唯一hash值来代替文件名 hash: this.container.hash + "-" + index })); await this.uploadChunks(uploadedList); } // 上传切片 async uploadChunks(uploadedList = []) { // 需要把requestList放到全局,因为要通过操控requestList来实现中断 this.requestList = this.data // 过滤出来未上传的切片 .filter(({ hash }) => !uploadedList.includes(hash)) // 构造formData .map(({ chunk,hash }) => { const formData = new FormData(); formData.append("chunk", chunk); formData.append("hash", hash); formData.append("filename", this.container.file.name); return { formData }; }) // 发送请求 上传切片 .map(async ({ formData }, index) => request(formData).then(() => { // 将请求成功的请求剥离出requestList this.requestList.splice(index, 1) }) ); await Promise.all(this.requestList); // 等待全部切片上传完毕 // 合并之前添加一层验证 验证全部切片传送完毕 if(uploadedList.length + this.requestList.length == this.data.length){ await merge(this.container.file.name) // 发送请求合并文件 } }, // 暂停上传 handlePause() { this.requestList.forEach((req) => { // 为每个请求新建一个AbortController实例 const controller = new AbortController(); req.signal = controller.signal controller.abort() }) } // 恢复上传 async handleRecovery() { //获取已上传切片列表 (verify为后端验证接口请求) const { uploadedList } = await verify(this.container.hash) await uploadChunks(uploadedList) }
添加功能总结
- 1.文件秒传其实就是一个简单的验证,把文件的hash发送给后端,后端验证是否存在该文件后将结果返回,如果存在则提示文件秒传成功
- 2.断点传送分为两步,暂停上传和恢复上传。暂停上传是通过获取到未上传完毕切片列表(完整切片列表剥离请求已完成的切片后形成),对列表请求进行请求中断实现的。恢复上传实质也是一层验证,在上传文件之前,将文件的hash发送给后端,后端返回已经上传完毕的切片列表,然后根据切片hash将后端返回的切片列表中的切片过滤出去,只上传未上传完成的切片。
加载全部内容