Skip to content

大文件上传

  • 对文件做切片:将一个文件拆分成多个文件做请求

  • 通知服务器合并切片:当所有切片上传完毕, 通知服务器合并切片

  • 控制多个请求的并发量:控制同时上传切片请求的数量, 防止浏览器卡死

  • 做断点续传:当有切片传输失败,需要对这些切片做处理,让它重新发送

切片

html
<body>
  <input type="file" id="fileInput" />
  <button id="uploadBtn">上传</button>
</body>

1. 读取文件

js
// 要传输的文件
var file = null;
// 获取上传完的文件
document.getElementById("fileInput").onchange = function (e) {
  file = e.target.files[0];
};

2. 对文件进行切片

文件 FIle 对象是 Blob 对象的子类,通过 slice 方法,可以对二进制文件进行拆分

js
function createSlice(file) {
  if (!file) return;
  let size = 1024 * 1024 * 2; //2MB 定义每份切片大小
  let fileChunks = [];
  let index = 0; //切片序号
  // 将切片的文件放入数组容器中;
  for (let cur = 0; cur < file.size; cur += size) {
    fileChunks.push({
      hash: index++,
      chunk: file.slice(cur, cur + size),
    });
  }
  return fileChunks;
}

3. 创建请求上传切片

js
document.getElementById("uploadBtn").onclick = async () => {
  // 创建切片
  const fileChunks = createSlice(file);
  // 为每个切片创建传送请求
  const uploadList = fileChunks.map((item, index) => {
    let formData = new FormData();
    formData.append("filename", file.name);
    formData.append("hash", item.hash);
    formData.append("chunk", item.chunk);
    return axios({
      method: "post",
      url: "/upload",
      data: formData,
    });
  });
  // 等所有切片传输完毕
  await Promise.all(uploadList);
  // 通知服务器切片传输完毕, 可以合并切片
  await axios({
    method: "get",
    url: "/merge",
    params: {
      filename: file.name,
    },
  });
  console.log("上传完成");
};

控制并发

建立并发池, 设立最大并发量

js
document.getElementById("uploadBtn").onclick = async () => {
  // 创建切片
  const fileChunks = createSlice(file);

  let pool = []; //并发池
  let max = 3; //最大并发量
  for (let i = 0; i < fileChunks.length; i++) {
    let item = fileChunks[i];
    let formData = new FormData();
    formData.append("filename", file.name);
    formData.append("hash", item.hash);
    formData.append("chunk", item.chunk);
    // 创建单个切片请求任务
    let task = axios({
      method: "post",
      url: "/upload",
      data: formData,
    });
    task.then((data) => {
      // 请求结束后将该Promise任务从并发池中移除
      let index = pool.findIndex((t) => t === task);
      pool.splice(index);
    });
    // 将切片任务塞进并发池执行
    pool.push(task);
    if (pool.length === max) {
      //并发池任务已满时, 等待释放空位
      await Promise.race(pool);
    }
  }
  //通知服务器合并切片
  await axios({
    method: "get",
    url: "/merge",
    params: {
      filename: file.name,
    },
  });
  console.log("上传完成");
};

断点续传

建立上传失败列表, 重复上传,直至所有切片上传成功

js
async function uploadFileChunks(list) {
  if (list.length === 0) {
    //所有任务完成,合并切片
    axios({
      method: "get",
      url: "/merge",
      params: {
        filename: file.name,
      },
    });
    console.log("上传完成");
    return;
  }
  let pool = []; //并发池
  let max = 3; //最大并发量
  let finish = 0; //完成的数量
  let failList = []; //失败的列表
  for (let i = 0; i < list.length; i++) {
    let item = list[i];
    let formData = new FormData();
    formData.append("filename", file.name);
    formData.append("hash", item.hash);
    formData.append("chunk", item.chunk);
    // 上传切片
    let task = axios({
      method: "post",
      url: "/upload",
      data: formData,
    });
    task
      .then((data) => {
        //请求结束后将该Promise任务从并发池中移除
        let index = pool.findIndex((t) => t === task);
        pool.splice(index);
      })
      .catch(() => {
        failList.push(item);
      })
      .finally(() => {
        finish++;
        if (finish === list.length) {
          //请求全部执行结束, 开始下一轮重新传送失败的切片
          uploadFileChunks(failList);
        }
      });
    pool.push(task);
    if (pool.length === max) {
      //每当并发池跑完一个任务,就再塞入一个任务
      await Promise.race(pool);
    }
  }
}

调用:

js
document.getElementById("uploadBtn").onclick = () => {
  // 创建切片
  const fileChunks = createSlice(file);
  // 上传切片
  uploadFileChunks(fileChunks);
};