某一天 , 在逛某金的时候突然看到这篇文章 , 前端大文件上传 , 之前也研究过类似的原理 , 但是一直没能亲手做一次 , 始终感觉有点虚 , 最近花了点时间 , 精(熬)心(夜)准(肝)备(爆)了个例子 , 来和大家分享 。
本文代码:github

文章插图
问题Knowing the time available to provide a response can avoid problems with timeouts. Current implementations select times between 30 and 120 seconds
https://tools.ietf.org/id/draft-thomson-hybi-http-timeout-00.html
【捣鼓系列:前端大文件上传】如果一个文件太大 , 比如音视频数据、下载的excel表格等等 , 如果在上传的过程中 , 等待时间超过30 ~ 120s , 服务器没有数据返回 , 就有可能被认为超时 , 这是上传的文件就会被中断 。
另外一个问题是 , 在大文件上传的过程中 , 上传到服务器的数据因为服务器问题或者其他的网络问题导致中断、超时 , 这是上传的数据将不会被保存 , 造成上传的浪费 。
原理大文件上传利用将大文件分片的原则 , 将一个大文件拆分成几个小的文件分别上传 , 然后在小文件上传完成之后 , 通知服务器进行文件合并 , 至此完成大文件上传 。
这种方式的上传解决了几个问题:
- 文件太大导致的请求超时
- 将一个请求拆分成多个请求(现在比较流行的浏览器 , 一般默认的数量是6个 , 同源请求并发上传的数量) , 增加并发数 , 提升了文件传输的速度
- 小文件的数据便于服务器保存 , 如果发生网络中断 , 下次上传时 , 已经上传的数据可以不再上传
File接口是基于Blob的 , 因此我们可以将上传的文件对象使用slice方法 进行分割 , 具体的实现如下:export const slice = (file, piece = CHUNK_SIZE) => {return new Promise((resolve, reject) => {let totalSize = file.size;const chunks = [];const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;let start = 0;const end = start + piece >= totalSize ? totalSize : start + piece;while (start < totalSize) {const chunk = blobSlice.call(file, start, end);chunks.push(chunk);start = end;const end = start + piece >= totalSize ? totalSize : start + piece;}resolve(chunks);});};然后将每个小的文件 , 使用表单的方式上传_chunkUploadTask(chunks) {for (let chunk of chunks) {const fd = new FormData();fd.append('chunk', chunk);return axios({url: '/upload',method: 'post',data: fd,}).then((res) => res.data).catch((err) => {});}}后端采用了express , 接收文件采用了[multer](https://github.com/expressjs/multer)这个 库multer上传的的方式有single、array、fields、none、any , 做单文件上传 , 采用single和array皆可 , 使用比较简便 , 通过req.file 或 req.files来拿到上传文件的信息另外需要通过
disk storage来定制化上传文件的文件名 , 保证在每个上传的文件chunk都是唯一的 。const storage = multer.diskStorage({destination: uploadTmp,filename: (req, file, cb) => {// 指定返回的文件名 , 如果不指定 , 默认会随机生成cb(null, file.fieldname);},});const multerUpload = multer({ storage });// routerrouter.post('/upload', multerUpload.any(), uploadService.uploadChunk);// serviceuploadChunk: async (req, res) => {const file = req.files[0];const chunkName = file.filename;try {const checksum = req.body.checksum;const chunkId = req.body.chunkId;const message = Messages.success(modules.UPLOAD, actions.UPLOAD, chunkName);logger.info(message);res.json({ code: 200, message });} catch (err) {const errMessage = Messages.fail(modules.UPLOAD, actions.UPLOAD, err);logger.error(errMessage);res.json({ code: 500, message: errMessage });res.status(500);}}上传的文件会被保存在uploads/tmp下 , 这里是由multer自动帮我们完成的 , 成功之后 , 通过req.files能够获取到文件的信息 , 包括chunk的名称、路径等等 , 方便做后续的存库处理 。为什么要保证chunk的文件名唯一?
- 因为文件名是随机的 , 代表着一旦发生网络中断 , 如果上传的分片还没有完成 , 这时数据库也不会有相应的存片记录 , 导致在下次上传的时候找不到分片 。这样的后果是 , 会在
tmp目录下存在着很多游离的分片 , 而得不到删除 。 - 同时在上传暂停的时候 , 也能根据chunk的名称来删除相应的临时分片(这步可以不需要 ,
multer判断分片存在的时候 , 会自动覆盖)
- 在做文件切割的时候 , 给每个chunk生成文件指纹 (
chunkmd5) - 通过整个文件的文件指纹 , 加上chunk的序列号指定(
filemd5+chunkIndex)
// 修改上述的代码const chunkName = `${chunkIndex}.${filemd5}.chunk`;const fd = new FormData();fd.append(chunkName, chunk);至此分片上传就大致完成了 。文件合并文件合并 , 就是将上传的文件分片分别读取出来 , 然后整合成一个新的文件 , 比较耗IO , 可以在一个新的线程中去整合 。
for (let chunkId = 0; chunkId < chunks; chunkId++) {const file = `${uploadTmp}/${chunkId}.${checksum}.chunk`;const content = await fsPromises.readFile(file);logger.info(Messages.success(modules.UPLOAD, actions.GET, file));try {await fsPromises.access(path, fs.constants.F_OK);await appendFile({ path, content, file, checksum, chunkId });if (chunkId === chunks - 1) {res.json({ code: 200, message });}} catch (err) {await createFile({ path, content, file, checksum, chunkId });}}Promise.all(tasks).then(() => {// when status in uploading, can send /makefile request// if not, when status in canceled, send request will delete chunk which has uploaded.if (this.status === fileStatus.UPLOADING) {const data = https://tazarkount.com/read/{ chunks: this.chunks.length, filename, checksum: this.checksum };axios({url:'/makefile',method: 'post',data,}).then((res) => {if (res.data.code === 200) {this._setDoneProgress(this.checksum, fileStatus.DONE);toastr.success(`file ${filename} upload successfully!`);}}).catch((err) => {console.error(err);toastr.error(`file ${filename} upload failed!`);});}});- 首先使用access判断分片是否存在 , 如果不存在 , 则创建新文件并读取分片内容
- 如果chunk文件存在 , 则读取内容到文件中
- 每个chunk读取成功之后 , 删除chunk
- 如果一个文件切割出来只有一个chunk , 那么就需要在
createFile的时候进行返回 , 否则请求一直处于pending状态 。
await createFile({ path, content, file, checksum, chunkId });if (chunks.length === 1) {res.json({ code: 200, message });} makefile之前务必要判断文件是否是上传状态 , 不然在cancel的状态下 , 还会继续上传 , 导致chunk上传之后 , chunk文件被删除 , 但是在数据库中却存在记录 , 这样合并出来的文件是有问题的 。

文章插图
如何做到文件秒传 , 思考三秒 , 公布答案 , 3. 2. 1..... , 其实只是个障眼法 。
为啥说是个障眼法 , 因为根本就没有传 , 文件是从服务器来的 。这就有几个问题需要弄清楚 ,
- 怎么确定文件是服务器中已经存在了的?
- 文件的上传的信息是保存在数据库中还是客户端?
- 文件名不相同 , 内容相同 , 应该怎么处理?
可以为每个文件上传生成对应的指纹 , 但是如果文件太大 , 客户端生成指纹的时间将大大增加 , 怎么解决这个问题?
还记得之前的
slice , 文件切片么?大文件不好做 , 同样的思路 , 切成小文件 , 然后计算md5值就好了 。这里使用spark-md5这个库来生成文件hash 。改造上面的slice方法 。export const checkSum = (file, piece = CHUNK_SIZE) => {return new Promise((resolve, reject) => {let totalSize = file.size;let start = 0;const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;const chunks = [];const spark = new SparkMD5.ArrayBuffer();const fileReader = new FileReader();const loadNext = () => {const end = start + piece >= totalSize ? totalSize : start + piece;const chunk = blobSlice.call(file, start, end);start = end;chunks.push(chunk);fileReader.readAsArrayBuffer(chunk);};fileReader.onload = (event) => {spark.append(event.target.result);if (start < totalSize) {loadNext();} else {const checksum = spark.end();resolve({ chunks, checksum });}};fileReader.onerror = () => {console.warn('oops, something went wrong.');reject();};loadNext();});};问题二:文件的上传的信息是保存在数据库中还是客户端?文件上传的信息最好是保存在服务端的数据库中(客户端可以使用
IndexDB) , 这样做有几个优点 , - 数据库服务提供了成套的
CRUD, 方便数据的操作 - 当用户刷新浏览器之后 , 或者更换浏览器之后 , 文件上传的信息不会丢失
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
