前言
文件上传的场景经常碰到,如果我们上传的文件是一个很大的文件,那么上传的时间应该会比较久,再加上网络不稳定各种因素的影响,很容易导致传输中断,用户除了重新上传文件外没有其他的办法,但是我们可以使用分片上传来解决这个问题。通过分片上传技术,如果网络传输中断,我们重新选择文件只需要传剩余的分片。而不需要重传整个文件,大大减少了重传的开销。
项目
分析流程图
- 流程简述
- 选择或拖拽文件,选择适当的分片大小
- 生成哈希加密散列
- 哈希校验(未上传,断点续传,已上传)
- 选择对应请求协议并发上传
- 文件分片上传完成,服务器合并分片
项目地址
https://github.com/xbc30/large-file-upload
详细目录介绍
├─client # vue client根目录
│ ├─src # 组件根目录
│ ├─assets # 静态文件svg
│ ├─components # 组件目录
│ ├─upload.vue # 上传组件
│ ├─App.vue # 根组件
├─server # eggjs server根目录
│ ├─app # 请求处理目录
│ ├─controller # controller目录
│ ├─home.js # 请求处理controller
│ ├─io # io目录
│ ├─controller # io/controller目录
│ ├─nsp.js # io请求处理controller
│ ├─router.js # 路由
│ ├─utils.js # 工具类函数
│ ├─config # 配置
│ ├─uploads # 上传文件目录
│ ├─e4e4-2097152 # 上传文件对应哈希目录
| |—build.jpg # 最终合并文件
├─README.md # README
亮点
- HTTP/WebSocket协议可供选择
- 选择或拖拽文件,并显示文件具体信息
- 进度条显示实时进度,友好的操作提示信息
- 根据文件大小自动匹配合适的分片大小,也可自定义分片大小
- 支持断点续传
- 少于等于10个分片请求就执行并行上传,大于10个分片请求就执行顺序同步上传
- 管道流合并分片减轻服务器压力
基本知识点
浏览器对象
Blob
将读取的文件进行分片切割,以拼凑准备上传的数据对象
const blobSlice =
File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
blobSlice.call(file, start, end) // 赋值到FormData示例的file属性
FileReader
使用FileReader.readAsArrayBuffer()
读取分片内容,以计算文件哈希
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
// readAsArrayBuffer() 方法用于启动读取指定的 Blob 或 File 内容
NodeJS模块
fs
fs
负责读取文件路径,检索文件数量,创建可读流,可写流
stream
管道流的stream
使用少量内存处理多个文件分片的合并
-
highWaterMark
默认是16kb,调整为每个分片大小的值,使得合并过程中数据不会丢失或损坏,充当缓存池 -
pipe
数据流中的积压问题
const input = fs.createReadStream(path, {
highWaterMark: chunkSize
});
input.pipe(output);
具体开发流程
哈希生成与校验
Blob
负责将文件按分片大小切分,FileReader
会按字节读取文件内容,并转换为ArrayBuffer
对象(二进制缓冲区),SparkMD5
负责计算文件最终哈希
// 计算文件哈希
calFileHash(file) {
return new Promise((resolve, reject) => {
const chunks = Math.ceil(file.size / this.chunkSize);
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
const that = this;
let currentChunk = 0;
fileReader.onload = function (e) {
console.log("read chunk nr", currentChunk + 1, "of", chunks);
spark.append(e.target.result); // Append array buffer
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
that.chunkTotal = currentChunk;
const hash = spark.end();
that.updateTips("success", "加载文件成功,文件哈希为" + hash);
resolve(hash);
}
};
fileReader.onerror