大文件上传的实现
准备阶段:
大文件上传的过程中需要用到发布订阅模式监听上传的进度
发布订阅模式的实现
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用于接受formdatarouter.post('/upload', upload.single('image'), (req, res) => { res.send({ code: 0, file: req.file.size });});module.exports = router;
效果如图 :