前言
近期做项目遇到一个文件过大上传很慢的问题,场景是上传一个几万条数据的Excel,文件大小达到1G。
切片上传
文件File都是属于Blob对象,Blob对象有一个重要方法slice,利用这个方法来实现对二进制文件的切片拆分。 具体代码如下:
<input type="file" onChange={uploadFile} />
const uploadFile = async (e) => {
const file = e.target.files[0];
const fileName = file.name;
const afterName = fileName.split(".")[fileName.split(".").length - 1]; // 文件后缀
const beforeName = fileName.split(".").slice(0, fileName.split(".").length - 1).join('.'); // 文件前缀名
const chunkSize = 3 * 1024; // 每个切片3k
let chunks = [];
if (file.size <= chunkSize) {
chunks.push(file.slice(0));
} else {
for (let i = 0; i < Math.ceil(file.size / chunkSize); i++) {
chunks.push(file.slice(i * chunkSize, (i + 1) * chunkSize)); // 使用slice方法进行拆分
}
}
for (let i = 0; i < chunks.length; i++) {
let formData = new FormData();
formData.append('file', chunks[i]);
formData.append('index', i);
formData.append('fileName', beforeName);
formData.append('afterName', afterName);
let task = request('http://localhost:3000/upload', { // 调接口上传
method: 'POST',
data: formData
});
}
}
express服务端
使用express框架简单创建一个上传文件接口。
const express = require('express');
var app = express();
var formidable = require('formidable');
fs = require('fs');
const { Buffer } = require('buffer');
const CURRENT_STATIC = 'static/current'; // 临时文件地址
const FILE_STATIC = 'static/files'; // 最终文件地址
app.post('/upload', function (req, res) {
res.set('Access-Control-Allow-Origin', '*');
var form = new formidable.IncomingForm();
form.parse(req, function (error, fields, files) {
const { fileName, afterName, index } = fields;
fs.exists(`${CURRENT_STATIC}/${fileName}`, function (exists) {
if (!exists) {
fs.mkdirSync(`${CURRENT_STATIC}/${fileName}`);
}
fs.writeFileSync(`${CURRENT_STATIC}/${fileName}/${fileName}${index || 0}${!!afterName ? '.' : ''}${afterName}`, fs.readFileSync(files.file.filepath)); // 将切片文件存入临时位置
res.send('分片文件保存成功');
})
})
});
切片合并
文件合并,这里我们要在服务端新增一个新的合并文件接口。
app.get('/merge', function (req, res) {
const { fileName, afterName } = req.query || {};
const fileList = fs.readdirSync(`${CURRENT_STATIC}/${fileName}`) || []; // 获取所有切片文件
let fileLength = 0;
let bufferList = [];
fileList.forEach(item => {
const buffer = fs.readFileSync(`${CURRENT_STATIC}/${fileName}/${item}`);
bufferList.push(buffer);
fileLength += buffer.length;
})
const buffer = Buffer.concat(bufferList, fileLength); // 合并切片文件
const ws = fs.createWriteStream(`${FILE_STATIC}/${fileName}${!!afterName ? '.' : ''}${afterName}${afterName}`);
ws.write(buffer); // 写入最终文件
fs.exists(`${CURRENT_STATIC}/${fileName}`, function (exists) {
if (!exists) return;
fs.rmdirSync(`${CURRENT_STATIC}/${fileName}`, { recursive: true }); // 删除切片文件
})
res.set('Access-Control-Allow-Origin', '*');
res.send('文件合并完成');
})
并发控制
这里主要利用Promise.race来控制多个请求同时并发的现象
let max = 4; //最大并发量
let pool = []; // 执行栈
for (let i = 0; i < chunks.length; i++) {
let formData = new FormData();
formData.append('file', chunks[i]);
formData.append('index', i);
formData.append('fileName', beforeName);
formData.append('afterName', afterName);
let task = request('http://localhost:3000/upload', {
method: 'POST',
data: formData
});
pool.push(task); // 将任务加入执行栈
task.then(() => {
let index = pool.findIndex(item => item === task);
pool.splice(index); // 任务完成将任务删除
})
if (pool.length >= max) { // 超过最大并发量进行等待
await Promise.race(pool);
}
}