这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

大文件上传主要步骤:

  1. 获取文件对象,切分文件
  2. 根据文件切片,计算文件唯一hash值
  3. 上传文件切片,服务端保存起来
  4. 合并文件切片,前端发送合并请求,服务端将文件切片合并为原始文件
  5. 秒传,对于已经存在的分片,可以前端发个请求获取已经上传的文件切片信息,前端判断已经上传的切片不再发送切片上传请求;或者后端验证已经存在的切片,直接返回成功结果,后端不再重复写入保存
  6. 暂停上传,使用axios的取消请求
  7. 继续上传,跟秒传逻辑一样,先发个请求验证,已经上传的切片不再重复发请求,将没有上传的切片继续上传

技术栈:

包管理工具:

  • pnpm

前端:

  • vue 3.3.11
  • vite
  • axios
  • spark-md5:根据文件内容生成唯一hash值

后端:

1、文件分片

先用vite搭一个vue3项目 pnpm create vite

首先拿到上传的文件,通过 <input type="file"/>change事件拿到File文件对象,File继承自Blob,可以调用Blob的实例方法,然后用slice方法做分割;

这篇文章介绍了 JS中的二进制对象:Blob、File、ArrayBuffer,及转换处理:FileReader、URL.createObjectURL

// App.vue
<script setup>
import { ref } from "vue";
import { createChunks } from "./utils"; // 保存切片
const fileChunks = ref([]); function handleFileChange(e) {
// 获取文件对象
const file = e.target.files[0];
if (!file) {
return;
}
fileChunks.value = createChunks(file);
console.log(fileChunks.value);
}
</script> <template>
<input type="file" @change="handleFileChange" />
<button>上传</button>
</template> // utils.js // 默认每个切片3MB
const CHUNK_SIZE = 3 * 1024 * 1024; export function createChunks(file, size = CHUNK_SIZE) {
const chunks = [];
for (let i = 0; i < file.size; i += size) {
chunks.push(file.slice(i, i + size));
}
return chunks;
}

2、计算hash

上传文件给服务器,要区分一下不同文件,对于服务端已经存在的文件切片,前端不需要重复上传,服务器不需要重复处理,节约性能。要做到区分不同文件,就需要给每个文件一个唯一标识,用 spark-md5 这个库来根据文件内容生成唯一hash值,安装 pnpm add spark-md5

// App.vue
<script setup>
import { ref } from "vue";
import { createChunks, calculateFileHash } from "./utils"; const fileChunks = ref([]); async function handleFileChange(e) {
const file = e.target.files[0];
if (!file) {
return;
}
fileChunks.value = createChunks(file);
const sT = Date.now();
const hash = await calculateFileHash(fileChunks.value);
console.log(Date.now() - sT); //测试一下计算hash耗时
} </script>
// utils.js
import SparkMD5 from "spark-md5"; export function calculateFileHash(chunkList) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
// FileReader读取文件内容
const reader = new FileReader();
reader.readAsArrayBuffer(new Blob(chunkList));
// 读取成功回调
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
});
}

上面calculateFileHash这个函数计算hash使用文件所有切片内容,如果文件很大,将会非常耗时,测试了一个526MB的文件,需要6813ms左右,为了保证所有切片都参与计算,也不至于太耗时,采取下面这种方式:

  • 第一个和最后一个切片全部计算
  • 其他切片取前、中、后两个字节参与计算

这种方式可能会损失一点准确性,如果计算出来的hash变了,就重新上传呗

// utils.js
const CHUNK_SIZE = 3 * 1024 * 1024; export function calculateFileHash(chunkList) {
return new Promise((resolve) => {
const spark = new SparkMD5.ArrayBuffer();
const reader = new FileReader();
// 抽取chunk
const chunks = [];
for (let i = 0; i < chunkList.length; i++) {
const chunk = chunkList[i];
if (i === 0 || i === chunkList.length - 1) {
chunks.push(chunk);
} else {
chunks.push(chunk.slice(0, 2));
chunks.push(chunk.slice(CHUNK_SIZE / 2, CHUNK_SIZE / 2 + 2));
chunks.push(chunk.slice(CHUNK_SIZE - 2, CHUNK_SIZE));
}
}
reader.readAsArrayBuffer(new Blob(chunks));
reader.onload = (e) => {
spark.append(e.target.result);
resolve(spark.end());
};
});
}

再次传同一个文件测试,只需要975ms左右

3、上传切片

前端逻辑:

这里要考虑一个问题,如果一个大文件切成了几十上百个切片,这时如果同时发送,浏览器负担很重,浏览器默认允许同时建立 6 个 TCP 持久连接,也就是说同一个域名同时能支持6个http请求,多余的会排队。这里就需要控制一下并发请求数量,设置为同时发送6个

// App.vue
<script setup>
import {
createChunks,
calculateFileHash,
createFormData,
concurrentChunksUpload,
} from "./utils"; async function uploadChunks() {
const hash = await calculateFileHash(fileChunks.value);
// 利用计算的文件hash构造formData
const dataList = createFormData(fileChunks.value, hash);
// 切片上传请求
await concurrentChunksUpload(dataList);
}
</script <template>
<input type="file" @change="handleFileChange" />
<button @click="uploadChunks()">上传</button>
</template>
// utils.js

const CHUNK_SIZE = 10 * 1024 * 1024;
const BASE_URL = "http://localhost:2024"; // 根据切片的数量组装相同数量的formData
export function createFormData(fileChunks, hash) {
return fileChunks
.map((chunk, index) => ({
fileHash: hash,
chunkHash: `${hash}-${index}`,
chunk,
}))
.map(({ fileHash, chunkHash, chunk }) => {
const formData = new FormData();
formData.append("fileHash", fileHash);
formData.append("chunkHash", chunkHash);
formData.append(`chunk-${chunkHash}`, chunk);
return formData;
});
} // 默认最大同时发送6个请求
export function concurrentChunksUpload(dataList, max = 6) {
return new Promise((resolve) => {
if (dataList.length === 0) {
resolve([]);
return;
}
const dataLength = dataList.length;
// 保存所有成功结果
const results = [];
// 下一个请求
let next = 0;
// 请求完成数量
let finished = 0; async function _request() {
// next达到dataList个数,就停止
if (next === dataLength) {
return;
}
const i = next;
next++; const formData = dataList[i];
const url = `${BASE_URL}/upload-chunks`;
try {
const res = await axios.post(url, formData);
results[i] = res.data;
finished++;
// 所有切片上传成功返回
if (finished === dataLength) {
resolve(results);
}
_request();
} catch (err) {
console.log(err);
}
}
// 最大并发数如果大于formData个数,取最小数
const minTimes = Math.min(max, dataLength);
for (let i = 0; i < minTimes; i++) {
_request();
}
});
}

后端逻辑:

浏览器跨域问题及几种常见解决方案:CORS,JSONP,Node代理,Nginx反向代理 ,分析如何解决浏览器跨域

const path = require("path");
const fs = require("fs");
const Koa = require("koa");
const KoaRouter = require("@koa/router");
const cors = require("@koa/cors");
const { koaBody } = require("koa-body"); const app = new Koa();
const router = new KoaRouter();
// 保存切片目录
const chunksDir = path.resolve(__dirname, "../chunks"); //cors解决跨域
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(2024, () => console.log("Koa文件服务器启动")); // 中间件:处理multipart/form-data,切片写入磁盘
const uploadKoaBody = koaBody({
multipart: true,
formidable: {
// 设置保存切片的文件夹
uploadDir: chunksDir,
// 在保存到磁盘前回调
onFileBegin(name, file) {
if (!fs.existsSync(chunksDir)) {
fs.mkdirSync(chunksDir);
}
// 切片重命名
file.filepath = `${chunksDir}/${name}`;
},
},
}); // 上传chunks切片接口
router.post("/upload-chunks", uploadKoaBody, (ctx) => {
ctx.body = { code: 200, msg: "文件上传成功" };
});

4、合并切片

前端逻辑:

当所有切片上传成功,发送合并请求

// App.vue
<script setup>
import {
createChunks,
calculateFileHash,
createFormData,
concurrentChunksUpload,
mergeChunks
} from "./utils"; async function uploadChunks() {
const hash = await calculateFileHash(fileChunks.value);
// 利用计算的文件hash构造formData
const dataList = createFormData(fileChunks.value, hash);
// 切片上传请求
await concurrentChunksUpload(dataList);
// 等所有chunks发送完毕,发送合并请求
mergeChunks(originFile.value.name);
}
</script <template>
<input type="file" @change="handleFileChange" />
<button @click="uploadChunks()">上传</button>
</template>

// utils.js

export function mergeChunks(filename) {
return axios.post(BASE_URL + "/merge-chunks", { filename, size: CHUNK_SIZE });
}

后端逻辑:

// 合并chunks接口
router.post("/merge-chunks", koaBody(), async (ctx) => {
const { filename, size } = ctx.request.body;
await mergeChunks(filename, size);
ctx.body = { code: 200, msg: "合并成功" };
}); // 合并 chunks
async function mergeChunks(filename, size) {
// 读取chunks目录中的文件名
const chunksName = fs.readdirSync(chunksDir);
if (!chunksName.length) return;
// 保证切片合并顺序
chunksName.sort((a, b) => a.split("-")[2] - b.split("-")[2]);
// 提前创建要写入的static目录
const fileDir = path.resolve(__dirname, "../static");
if (!fs.existsSync(fileDir)) {
fs.mkdirSync(fileDir);
}
// 最后写入的文件路径
const filePath = path.resolve(fileDir, filename);
const pipeStreams = chunksName.map((chunkName, index) => {
const chunkPath = path.resolve(chunksDir, chunkName);
// 创建写入流
const writeStream = fs.createWriteStream(filePath, { start: index * size });
return createPipeStream(chunkPath, writeStream);
});
await Promise.all(pipeStreams);
// 全部写完,删除chunks切片目录
fs.rmdirSync(chunksDir);
} // 创建管道流写入
function createPipeStream(chunkPath, writeStream) {
return new Promise((resolve) => {
const readStream = fs.createReadStream(chunkPath);
readStream.pipe(writeStream);
readStream.on("end", () => {
// 写完一个chunk,就删除
fs.unlinkSync(chunkPath);
resolve();
});
});
}

5、秒传文件

对于已经上传的文件,服务端这边可以判断,直接返回成功结果,不做重复保存的处理,节省时间;也可以前端先发一个请求获取已经上传的文件切片,就不再重复发送切片上传请求

服务端逻辑加一个中间件做判断:

// 中间件,已经存在的切片,直接返回成功结果
async function verifyChunks(ctx, next) {
// 前端把切片hash放到请求路径上带过来
const chunkName = ctx.request.querystring.split("=")[1];
const chunkPath = path.resolve(chunksDir, chunkName);
if (fs.existsSync(chunkPath)) {
ctx.body = { code: 200, msg: "文件已上传" };
} else {
await next();
}
} // 上传chunks切片接口
router.post("/upload-chunks", verifyChunks, uploadKoaBody, (ctx) => {
ctx.body = { code: 200, msg: "文件上传成功" };
});

前端这边修改一下请求路径,带个参数过去

export function concurrentChunksUpload(dataList, max = 6) {
return new Promise((resolve) => {
//...
const formData = dataList[i];
const chunkName = `chunk-${formData.get("chunkHash")}`;
const url = `${BASE_URL}/upload-chunks?chunkName=${chunkName}`;
//...
});
}

6、暂停上传

前端逻辑

axios中可以使用同一个 cancel token 取消多个请求

<script setup>
import axios from "axios"; const CancelToken = axios.CancelToken;
let axiosSource = CancelToken.source(); function pauseUpload() {
axiosSource.cancel?.();
} async function uploadChunks(existentChunks = []) {
const hash = await calculateFileHash(fileChunks.value);
const dataList = createFormData(fileChunks.value, hash, existentChunks);
await concurrentChunksUpload(axiosSource.token, dataList);
// 等所有chunks发送完毕,发送合并请求
mergeChunks(originFile.value.name);
}
</script> <template>
<input type="file" @change="handleFileChange" />
<button @click="uploadChunks()">上传</button>
<button @click="pauseUpload">暂停</button>
</template>
// utils.js

export function concurrentChunksUpload(sourceToken, dataList, max = 6) {
return new Promise((resolve) => {
//...
const res = await axios.post(url, formData, {
cancelToken: sourceToken,
});
//...
});
}

7、继续上传

前端逻辑

要调用CancelToken.source()重新生成一个suource,发请求获取已经上传的chunks,过滤一下,不再重复发送,前面的秒传是在服务端判断的,也可以按这个逻辑来,已经上传的不重复发请求

<script setup>
import { getExistentChunks } from "./utils"; async function continueUpload() {
const { data } = await getExistentChunks();
uploadChunks(data);
} // existentChunks 默认空数组
async function uploadChunks(existentChunks = []) {
const hash = await calculateFileHash(fileChunks.value);
// existentChunks传入过滤已经上传的切片
const dataList = createFormData(fileChunks.value, hash, existentChunks);
// 重新生成source
axiosSource = CancelToken.source();
await concurrentChunksUpload(axiosSource.token, dataList);
// 等所有chunks发送完毕,发送合并请求
mergeChunks(originFile.value.name);
} </script> <template>
<input type="file" @change="handleFileChange" />
<button @click="uploadChunks()">上传</button>
<button @click="pauseUpload">暂停</button>
<button @click="continueUpload">继续</button>
</template>
// utils.js
export function createFormData(fileChunks, hash, existentChunks) {
const existentChunksName = existentChunks
// 如果切片有损坏,切片大小可能就不等于CHUNK_SIZE,重新传
// 最后一张切片大小大概率是不等的
.filter((item) => item.size === CHUNK_SIZE)
.map((item) => item.filename); return fileChunks
.map((chunk, index) => ({
fileHash: hash,
chunkHash: `${hash}-${index}`,
chunk,
}))
.filter(({ chunkHash }) => {
// 同时过滤掉已经上传的切片
return !existentChunksName.includes(`chunk-${chunkHash}`);
})
.map(({ fileHash, chunkHash, chunk }) => {
const formData = new FormData();
formData.append("fileHash", fileHash);
formData.append("chunkHash", chunkHash);
formData.append(`chunk-${chunkHash}`, chunk);
return formData;
});
} export function getExistentChunks() {
return axios.post(BASE_URL + "/existent-chunks");
}

后端逻辑

// 获取已经上传的切片接口
router.post("/existent-chunks", (ctx) => {
if (!fs.existsSync(chunksDir)) {
ctx.body = [];
return;
}
ctx.body = fs.readdirSync(chunksDir).map((filename) => {
return {
// 切片名:chunk-tue234wdhfjksd211tyf3234-1
filename,
// 切片大小
size: fs.statSync(`${chunksDir}/${filename}`).size,
};
});
});

最后

  • 多次尝试一个23MB的pdf和一个536MB的mp4,重复几次点暂停和继续,最后都可以打开
  • 如有问题,请不吝指教,学习一下

本文转载于:

https://juejin.cn/post/7317704519160528923

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--经常被cue大文件上传,忍不住试一下的更多相关文章

  1. SourceTree --转载 SourceTree大文件上传提示POST git-receive-pack (chunked)相关问题记录

    前两天,更新了百度地图的SDK,更新完了通过SourceTree上传到Github 结果提示 :POST git-receive-pack (chunked), 在网上查询之后了解到这个提示的原因是因 ...

  2. 使用commons-fileupload包进行大文件上传注意事项

    项目中使用 commons-fileupload-1.2.1.jar 进行大文件上传. 测试了一把,效果很不错. 总结如下: 必须设置好上传文件的最大阀值 final long MAX_SIZE = ...

  3. php 大文件上传的实现

    最近公司做工程项目,实现大文件上传 网上找了很久,发现网上很多代码大都存在很多问题,不过还是让我找到了一个符合要求的项目. 工程: 对项目的文件上传功能做出分析,找出文件上传的原理,对文件的传输模式深 ...

  4. C# 大文件上传

    IHttpModule 分块上传大文件 IHttpModule 分块上传大文件 来源:http://www.cnblogs.com/HeroBeast/archive/2008/03/18/10848 ...

  5. 分享一篇关于C#大文件上传的整个过程

    简单写个小例子,记录一下此次大文件上传遇到的所有问题. 一.客户端(使用winform窗体实现) 具体功能: 点击“选择”按钮,选择要上传的文件 点击“上传文件”按钮,上传该文件调用UpLoad_Re ...

  6. vue大文件上传控件选哪个好?

    需求: 项目要支持大文件上传功能,经过讨论,初步将文件上传大小控制在20G内,因此自己需要在项目中进行文件上传部分的调整和配置,自己将大小都以20G来进行限制. PC端全平台支持,要求支持Window ...

  7. js实现大文件上传分片上传断点续传

    文件夹上传:从前端到后端 文件上传是 Web 开发肯定会碰到的问题,而文件夹上传则更加难缠.网上关于文件夹上传的资料多集中在前端,缺少对于后端的关注,然后讲某个后端框架文件上传的文章又不会涉及文件夹. ...

  8. php实现大文件上传分片上传断点续传

    前段时间做视频上传业务,通过网页上传视频到服务器. 视频大小 小则几十M,大则 1G+,以一般的HTTP请求发送数据的方式的话,会遇到的问题:1,文件过大,超出服务端的请求大小限制:2,请求时间过长, ...

  9. 使用webuploader实现大文件上传分片上传

    本人在2010年时使用swfupload为核心进行文件的批量上传的解决方案.见文章:WEB版一次选择多个文件进行批量上传(swfupload)的解决方案. 本人在2013年时使用plupload为核心 ...

  10. java大文件上传解决方案

    最近遇见一个需要上传百兆大文件的需求,调研了七牛和腾讯云的切片分段上传功能,因此在此整理前端大文件上传相关功能的实现. 在某些业务中,大文件上传是一个比较重要的交互场景,如上传入库比较大的Excel表 ...

随机推荐

  1. Python-pymysql查询MySQL的表

    一.安装pymysql py -m pip install pymysql; 二.创建表并插入数据 CREATE TABLE `course` ( `course_id` varchar(10) DE ...

  2. Go 之烧脑的接口

    基本定义 Go 官方对于接口的定义是一句话:An interface type is defined as a set of method signatures. 翻译过来就是,一个接口定义了一组方法 ...

  3. NC16655 [NOIP2005]过河

    题目链接 题目 题目描述 在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧.在桥上有一些石子,青蛙很讨厌踩在这些石子上.由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可 ...

  4. Js捕获异常的方法

    Js捕获异常的方法 JavaScript的异常主要使用try catch finally语句以及窗口对象window的onerror事件来捕获. try catch finally try catch ...

  5. golang微服务实践:分布式链路追踪系统-jaeger安装与简单使用

    简介 jaeger是一个比较有名的分布式链路追踪系统,底层用golang实现,兼容opentracing标准. 文档地址:docs github地址:github 官网:website blog:bl ...

  6. OFDM系统各种调制阶数的QAM误码率(Symbol Error Rate)与 误比特率(Bit Error Rate)仿真结果

    本文是OFDM系统的不同QAM调制阶数的误码率与误比特率仿真,仅考虑在高斯白噪声信道下的情景,着重分析不同信噪比下的误码(符号)率性能曲线,不关心具体的调制与解调方案,仿真结果与理论的误码率曲线进行了 ...

  7. 如何保证消息顺序执行(Rabbitmq/kafka)

    转载: https://www.cnblogs.com/-wenli/p/13047059.html https://www.jianshu.com/p/02fdcb9e8784

  8. 【ACM专项练习#02】输入整行字符串、输入值到vector、取输入整数的每一位

    输入整行字符串 平均绩点 题目描述 每门课的成绩分为A.B.C.D.F五个等级,为了计算平均绩点,规定A.B.C.D.F分别代表4分.3分.2分.1分.0分. 输入 有多组测试样例.每组输入数据占一行 ...

  9. 矩池云上 git clone --recursive 出错,怎么解决

    遇到问题 有时候安装包教程里 git clone 的时候会出现以下错误: git clone --recursive https://github.91chi.fun/https://github.c ...

  10. 终端SSH远程连接CentOS报错:-bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such file or directory

    终端SSH远程连接CentOS时,报以下错误提示: -bash: warning: setlocale: LC_CTYPE: cannot change locale (UTF-8): No such ...