字节跳动面试官:请你实现一个大文件上传和断点续传

前言

这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 😃

事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对

结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?

本文将从零搭建前端和服务端,实现一个大文件上传和断点续传

前端:Vue@2 + Element-ui

服务端:Nodejs@14 + multiparty

大文件上传

整体思路

前端

前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,文件的 slice 方法可以返回原文件的某个切片

预先定义好单个切片大小,将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片。这样从原本传一个大文件,变成了并发传多个小的文件切片,可以大大减少上传时间

另外由于是并发,传输到服务端的顺序可能会发生变化,因此我们还需要给每个切片记录顺序

服务端

服务端负责接受前端传输的切片,并在接收到所有切片后合并所有切片

这里又引伸出两个问题

  1. 何时合并切片,即切片什么时候传输完成
  2. 如何合并切片

第一个问题需要前端配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并。或者也可以额外发一个请求,主动通知服务端进行切片的合并

第二个问题,具体如何合并切片呢?这里可以使用 Nodejs 的 读写流(readStream/writeStream),将所有切片的流传输到最终文件的流里

talk is cheap,show me the code,接着我们用代码实现上面的思路

前端部分

前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 Element-ui 作为 UI 框架

上传控件

首先创建选择文件的控件并监听 change 事件,另外就是上传按钮

<template>
 ? <div>
 ? ?<input type="file" @change="handleFileChange" />
 ? ?<el-button @click="handleUpload">upload</el-button>
 ?</div>
</template>
?
<script>
export default {
 ?data: () => ({
 ? ?container: {
 ? ? ?file: null
 ?  }
  }),
 ?methods: {
 ? ? handleFileChange(e) {
 ? ? ?const [file] = e.target.files;
 ? ? ?if (!file) return;
 ? ? ?Object.assign(this.$data, this.$options.data());
 ? ? ?this.container.file = file;
 ?  },
 ? ?async handleUpload() {}
  }
};
</script>

请求逻辑

考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

request({
 ? ? ?url,
 ? ? ?method = "post",
 ? ? ?data,
 ? ? ?headers = {},
 ? ? ?requestList
 ?  }) {
 ? ? ?return new Promise(resolve => {
 ? ? ? ?const xhr = new XMLHttpRequest();
 ? ? ? ?xhr.open(method, url);
 ? ? ? ?Object.keys(headers).forEach(key =>
 ? ? ? ? ?xhr.setRequestHeader(key, headers[key])
 ? ? ?  );
 ? ? ? ?xhr.send(data);
 ? ? ? ?xhr.onload = e => {
 ? ? ? ? ?resolve({
 ? ? ? ? ? ?data: e.target.response
 ? ? ? ?  });
 ? ? ?  };
 ? ?  });
 ?  }

上传切片

接着实现比较重要的上传功能,上传需要做两件事

  • 对文件进行切片

  • 将切片传输给服务端

    ?

当点击上传按钮时,调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 10MB,也就是说一个 100 MB 的文件会被分成 10 个 10MB 的切片

createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 formData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

发送合并请求

使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并

前端发送额外的合并请求,服务端接受到请求时合并切片

<template>
  <div>
 ?  <input type="file" @change="handleFileChange" />
 ?  <el-button @click="handleUpload">upload</el-button>
  </div>
</template>
?
<script>
export default {
  data: () => ({
 ?  container: {
 ? ?  file: null
 ?  },
 ?  data: []
  }),
  methods: {
 ?  request() {},
 ?  handleFileChange() {},
 ?  createFileChunk() {},
 ?  async uploadChunks() {
 ? ?  const requestList = this.data
 ? ? ?  .map(({ chunk,hash }) => {
 ? ? ? ?  const formData = new FormData();
 ? ? ? ?  formData.append("chunk", chunk);
 ? ? ? ?  formData.append("hash", hash);
 ? ? ? ?  formData.append("filename", this.container.file.name);
 ? ? ? ?  return { formData };
 ? ? ?  })
 ? ? ?  .map(({ formData }) =>
 ? ? ? ?  this.request({
 ? ? ? ? ?  url: "http://localhost:3000",
 ? ? ? ? ?  data: formData
 ? ? ? ?  })
 ? ? ?  );
 ? ?  await Promise.all(requestList);
+ ? ? // 合并切片
+ ? ? await this.mergeRequest();
 ?  },
+ ?  async mergeRequest() {
+ ? ?  await this.request({
+ ? ? ?  url: "http://localhost:3000/merge",
+ ? ? ?  headers: {
+ ? ? ? ?  "content-type": "application/json"
+ ? ? ?  },
+ ? ? ?  data: JSON.stringify({
+ ? ? ? ?  filename: this.container.file.name
+ ? ? ?  })
+ ? ?  });
+ ?  }, ? ?
 ?  async handleUpload() {}
  }
};
</script>

服务端部分

使用 http 模块搭建一个简单服务端

const http = require("http");
const server = http.createServer();
?
server.on("request", async (req, res) => {
 ?res.setHeader("Access-Control-Allow-Origin", "*");
 ?res.setHeader("Access-Control-Allow-Headers", "*");
 ?if (req.method === "OPTIONS") {
 ? ?res.status = 200;
 ? ?res.end();
 ? ?return;
  }
});
?
server.listen(3000, () => console.log("listening port 3000"));

接受切片

使用multiparty处理前端传来的 formData

在 multiparty.parse 的回调中,files 参数保存了 formData 中文件,fields 参数保存了 formData 中非文件的字段

const http = require("http");
const path = require("path");
+ const fse = require("fs-extra");
+ const multiparty = require("multiparty");
?
const server = http.createServer();
+ // 大文件存储目录
+ const UPLOAD_DIR 
  • 24
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值