大文件上传的实现
准备阶段:
大文件上传的过程中需要用到发布订阅模式监听上传的进度
发布订阅模式的实现
export default class {
// 事件栈
eventStacks = [{
eventType: '',
handlers: []}];
/**
* 获取事件对应栈的索引
*
* @param {string} eventType 事件类型
* @return {number} stackIndex 对应栈的索引 不存在为 -1
*/
indexOf(eventType) {
const eventStacks = this.eventStacks;
// 已有事件类型处理栈
let stackIndex = -1;
for (let i = 0; i < eventStacks.length; i++) {const eventStack = eventStacks[i];
if (eventStack.eventType === eventType) {
stackIndex = i;
break;
}
}
return stackIndex;
};
/**
* 监听事件
*
* @param {string} eventType 自定义事件类型
* @param {Function} handler 事件处理函数
*/
on(eventType, handler) {const index = this.indexOf(eventType);
if (index >= 0) {
// 已存在事件类型处理 直接把相应的处理函数入栈
this.eventStacks[index].handlers.push(handler);
}
else {
// 不存在事件,把对应的事件处理入栈
const newEventStack = {
eventType,
handlers: [handler]
};
this.eventStacks.push(newEventStack);
}
};
/**
* 触发对应的事件
*
* @param {string} eventType 自定义事件类型
* @param {Object} params 参数对象
*/
emit(eventType, params = {}) {this.execEvent(eventType, params);
};
/**
* 执行对应的事件
*
* @param {string} eventType 自定义事件类型
* @param {Object} params 参数对象
*/
execEvent(eventType, params = {}) {const index = this.indexOf(eventType);
if (index < 0) {return;}
const handlers = this.eventStacks[index].handlers;
for (let i = 0; i < handlers.length; i++) {const currentHandler = handlers[i];
if (currentHandler && typeof currentHandler === 'function') {currentHandler(params);
}
}
};
/**
* 解除对应的事件
*
* @param {string} eventType 事件类型
* @param {Function} handler 事件处理器 必须是引用传进来 使用对象引用相等判断
*/
offHandler(eventType, handler) {const index = this.indexOf(eventType);
if (index >= 0 && this.eventStacks[index].handlers.length) {
// 存在,并且已经入栈
const handlers = this.eventStacks[index].handlers;
this.eventStacks[index].handlers = handlers.filter(currentHandler => {return currentHandler !== handler});
}
}
}
文件是基于 Blob BlobMDN 文档
Blob 类型可以使用 slice 截取一段 基于这点 我们就可以吧大文件拆分成很多小的文件块上传 前端实现如下:
import superagent from 'superagent';
import eventEmitter from '../util/eventEmitter';
// 上传单个文件
export const uploadFile = (file, url) => {const formData = new FormData();
formData.set('image', file);
console.log(file.size);
return superagent.post(url)
.send(formData);
};
// 获取文件分块之后的数量
const getFragmentNum = (file, fragmentSize) => {
// fragmentSize 拆分文件的大小
return Math.ceil(file.size / fragmentSize);
};
// 获取文件碎片
const getFileFragment = (file, start, end, fragmentSize) => {return file.slice(start * fragmentSize, end * fragmentSize);
};
const getSlicedFiles = (file, fragmentSize) => {const slicedFiles = [];
// 获取文件分块的数量
const fragmentNum = getFragmentNum(file, fragmentSize);
// 获取所有的分块文件
for (let i = 0; i < fragmentNum; i++) {const currentFileFragment = getFileFragment(file, i, i + 1, fragmentSize);
slicedFiles.push(currentFileFragment);
}
return slicedFiles;
};
// 多文件上传
export class uploadBigFile extends eventEmitter {constructor(file, url, fragmentSize) {super();
// 拆分的文件数组
const slicedFiles = getSlicedFiles(file, fragmentSize);
// 正在上传的模块
this.currentIndex = 0;
this.uploadSlicedFiles(slicedFiles, url);
};
/**
* 上传拆分后的文件
*
* @param {Array} files 文件数组
* @param {string} url 上传的地址
*/
uploadSlicedFiles(files, url) {uploadFile(files[this.currentIndex], url)
.then(({body}) => {
this.emit('process', {
currentIndex: this.currentIndex,
res: body,
progress: (this.currentIndex + 1) / files.length
});
this.currentIndex++;
if (this.currentIndex < files.length) {this.uploadSlicedFiles(files, url);
}
})
.catch(err => {console.log(err);
});
};
}
后端实现如下:
const express = require('express');
const router = express.Router();
const multer = require('multer');
const upload = multer();
// multer 用于接受 formdata
router.post('/upload', upload.single('image'), (req, res) => {
res.send({
code: 0,
file: req.file.size
});
});
module.exports = router;
效果如图: