前端大文件分片上传

263 阅读1分钟

前言

近期做项目遇到一个文件过大上传很慢的问题,场景是上传一个几万条数据的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);
   }
}

0142ae6a4ee109110eed479f0afbb571.jpg