前端实现大文件上传

大文件上传

实现思路

对于大文件上传考虑到上传时间太久、超出浏览器响应时间、提高上传效率、优化上传用户体验等问题进行了深入探讨,以下初略罗列各个知识点的实现思路:

  1. 大文件上传对文件本身进行了文件流内容 Blob 的分割,使用 Blob.prototype.slice 实现大文件的上传切分为多个小文件的上传
  2. 为了实现大文件上传能否做到秒传、辨别是否已存在、文件切片的秒传等功能,需要对大文件进行计算 Hash 的唯一标识,通过使用 WebWorker 开启浏览器线程来计算文件 Hash,防止阻塞 UI 渲染(另外也采用 React Fiber 所用的时间分片思想方式 requestIdleCallback API 来计算)
  3. 上传暂停/恢复功能采用 XMLHttpRequest 请求带有的 abort 方法进行请求的取消来实现
  4. 判断文件是否已存在,在性能上可以通过计算抽样 Hash 来大大缩短大文件全量计算 Hash 的时间,使用这个抽样 Hash 向服务器确认是否已存在文件,而达到秒传的功能,抽样 Hash 的作用在于牺牲一点点的识别率来换取时间
  5. 大文件切分为小文件后,通过设置一个上传通道限制,实现控制并发上传数来防止一次性过多的 HTTP 请求而卡死浏览器
  6. 文件切片上传采用请求 catch 捕获方式,来对上传失败的内容进行重试,重试三次后再失败就进行放弃
  7. 对文件服务器过期的文件切片开启定时器清理,采用了 node-schedule 来实现

上传切片

html 复制代码
<!-- 单选文件 -->
<input id="fileInput" type="file" />
js 复制代码
const fileInput = document.querySelector('#fileInput');

// 1. 点击输入框选择文件后触发
fileInput.addEventListener('change', e => {
  const [file] = e.target.files;
  if (!file) return;

  const chunkList = sliceFileChunk(file);
});

// 2. 文件切片
function sliceFileChunk(file) {
  // 文件大小
  const FILE_SIZE = file.size;
  // 文件切片大小
  const CHUNK_SIZE = 2 * 1024 * 1024;
  // 切片的个数
  const CHUNKS = Math.ceil(FILE_SIZE / CHUNK_SIZE);

  const blobSlice = Fil.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  // 生成 MD5
  const spark = new SparkMD5.ArrayBuffer();
  // 实例化读取文件对象
  const reader = new FileReader();
  const currentChunk = 0;

  reader.onload = function(e) {
    const resul = e.target.result;
    spark.append(result);
    currentChunk++;

    if (currentChunk < chunks) {
      loadNext();
      console.log(`第${currentChunk}个分片解析完成`);
    } else {
      const md5 = spark.end();
      console.log('解析完成');
    }
  };

  function loadNext() {
    const start = currentChunk * CHUNK_SIZE;
    const end = start + CHUNK_SIZE > file.size ? file.size : start + CHUNK_SIZE;

    reader.raedAsArrayBuffer(blobSlice.call(file, start, end));
  }

  loadNext();
}

// 上传切片
async function uploadChunkus() {
  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(async ({ formData }) => {
      return this.request({
        url: 'http://localhost:3000',
        data: formData,
      });
    });

  // 并发上传文件切片
  await Promise.all(requestList);
}

async function handleUpload() {}

发送合并请求

接受切片

js 复制代码
const http = require('http');
const path = require('path');
const fse = require('fs-extra');
const multiparty = require('multiparty');

const server = http.createServer();
// 大文件存储目录
const UPLOAD_DIR = path.resolve(__dirname, '..', 'target');

server.on('request', async (req, res) => {
  res.setHeader('Access-Control-Allow-Oriign', '*');
  res.setHeader('Access-Control-Allow-Headers', '*');

  if (req.method === 'OPTIONS') {
    res.status = 200;
    res.end();
    return;
  }

  const multipart = new multiparty.Form();

  multipart.parse(req, async (err, fields, files) => {
    if (err) return;

    const [chunk] = files.chunk;
    const [hash] = fields.hash;
    const [filename] = fields.filename;
    const chunkDir = path.resolve(UPLOAD_DIR, filename);

    // 切片目录不存在,创建切片目录
    if (!fse.existsSync(chunkDir)) {
      await fse.mkdirs(chunkDir);
    }

    // fs-extra 专用方法,类似 fs.rename 并且跨平台
    // fs-extra 的 rename 方法 windows 平台会有权限问题
    await fse.move(chunk.path, `${chunkDir}/${hash}`);

    res.end('Received file chunk');
  });
});

server.listen(3000, () => console.log('Server is listening port 3000.'));

合并切片

由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹。

接着使用 fs.createWriteStream 创建一个可写流,可写流文件名就是 切片文件夹名 + 后缀名 组合而成。

随后遍历整个切片文件夹,将切片通过 fs.createReadStream 创建可读流,传输合并到目标文件中。

值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream 的第二个参数 start/end 控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size 参数。

断点续传

断点续传的原理在于前端/服务端需要 记住 已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能:

  • 前端使用 localStorage 记录已上传的切片 hash
  • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

生成标识

无论是前端还是服务端,都必须要生成文件和切片的 Hash,之前我们使用 文件名 + 切片下标 作为切片 Hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,Hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 Hash 的生成规则。

相关推荐
wangruofeng2 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng2 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈5 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技5 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569155 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马8 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng8 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding9 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天10 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo12 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构