大文件上传

业务场景

当我们上传一个文件,当文件比较大的时候,需要上传的时间比较长,如果出现网络中断的场景,那么就需要重新的上传整个文件,这对于用户来说,是非常不好的体验,也非常浪费资源。 所以在大文件上传时,往往对 大文件 进行切片操作,前端负责将文件进行切片,然后单文件上传给后端,等后端拿到所有的切片,进行一个合并。

  1. 用户选择文件后点击上传按钮。
  2. 前端计算文件的哈希值。
  3. 检查文件是否已存在(秒传检查)。
  4. 如果文件不存在,将文件切片。
  5. 获取已上传的切片,生成待上传的切片队列。
  6. 控制并发数量上传切片。
  7. 所有切片上传完成后,合并切片。

整体流程

切片上传是如何实现的:

1.选择文件:用户在前端页面选择要上传的文件,将选择的文件进行保存

2.利用sparkmd5计算文件的哈希值:用于后续的秒传判断。

3.利用文件的slice方法,把一个大文件切片成很多个小文件push到一个数组里

4.利用form data,循环把每个切片都传递给后端(文件切片,文件名,索引值,总的切片数量)

5.判断已经上传的切片数量和总的切片数量是否相等,如相等,向后端发送合并文件的请求;

利用索引值来确定切片的顺序,又利用哈希值来验证文件唯一性,以提高文件上传系统的可靠性和安全性。

前端最核心的操作就是:如何对文件进行切片?

1.选择文件:用户在前端页面选择要上传的文件,将选择的文件进行保存。拿到的是一个文件对象。切片是一个blob 对象

利用文件的slice方法,把一个大文件切片成很多个小文件push到一个数组里

切片方法:传入的是文件 和每一个切片的大小。创建一个数组,用来保存每个切片。

秒传场景:刷新一下页面,问服务器,当前文件上传没有,哪些切片还没有上传

文件hash值的计算 异步封装

重点在于:怎么知道是这个文件,用一个东西去描述这个文件,

不能用文件名:文件名容易重复,路径会移动

找到唯一能代表这个文件的东西:文件hash,hash算法:将任何数据转化为一个固定长度的字符串,也是不可逆的。而且,里面任意数据的修改,都会导致产生不同的哈希值。

Md5算法 :如何在客户端去计算这个哈希值,使用的是第三方库 spark-md5

重点:不能一次性计算整个文件的哈希值,因为计算文件的哈希值,要拿到文件的整个数据,一个高清电影可能就100G了,内存拿到数据再去计算,吃不消。

要分块去算,增量算法,先拿一块数据计算,先计算这块,这块计算完了就不要了,下一块来了,和之前的结果计算一个新的结果。这块数据也不要了。

注意点:将文件hash值的计算,封装成异步的,需要花费一点时间去计算哈希值

尽管这样,还是可能卡顿:优化方案,使用webworker 去单独开一个线程

断点续传的实现

若不能秒传,向后端发送包含文件哈希值的请求,获取已上传的切片索引数组。前端根据这个数组确定哪些切片还需要上传。遍历切片的数组,如果当前切片的索引包含在已上传的数组中,就会跳过这个切片,进行下一个切片的判断。通过索引记录已上传的切片,重新上传时跳过已上传的部分来实现断点续传功能

秒传如何实现

上传前先发送文件 hash,检查是否存在相同哈希值且已完成上传的文件,服务端存在相同文件则直接返回,使用 spark-md5 计算文件 hash。

并发上传是通过以下方式实现的

并发上传指同时上传文件的多个切片以提升效率,实现方式多样:

  1. 队列方式 : - 原理 :利用队列存储待上传切片,结合计数器和并发限制值控制并发数量。上传任务完成后,从队列取新切片上传。 - 优点:逻辑清晰,能灵活控制并发数量。

  2. Promise.allPromise.allSettled : - 原理 :将切片封装成 Promise 对象,使用上述方法同时发起多个请求,需提前控制并发数量。 - 优点 :代码简洁,适合批量处理请求。 3. 第三方库(如 p - limit : - 原理 :借助库提供的并发控制功能,轻松管理并发数量。 - 优点:使用方便,减少手动控制复杂度。

  3. 分片队列

  • 使用 chunkQueue 数组存储所有分片的索引。
  • 每次从队列中取出一个分片进行上传。
javascript 复制代码
const chunkQueue = Array.from({ length: totalChunks }, (_, i) => i);
  1. 上传任务函数
  • 定义了 uploadNextChunk 函数,用于从队列中取出一个分片并上传。
  • 每次上传完成后,递归调用自身以继续处理下一个分片。
javascript 复制代码
const uploadNextChunk = async () => {
  if (chunkQueue.length === 0) return; // 队列为空时结束
  const chunkIndex = chunkQueue.shift(); // 从队列中取出一个分片索引
  // 上传逻辑...
  await uploadNextChunk(); // 继续上传下一个分片
};
  1. 并发控制
  • 使用 maxConcurrentUploads 控制并发数量。
  • 创建多个并发任务,每个任务调用 uploadNextChunk
  • 使用 Promise.all 等待所有并发任务完成。
javascript 复制代码
const uploaders = Array.from({ length: maxConcurrentUploads }, () =>
  uploadNextChunk()
);
await Promise.all(uploaders); // 等待所有并发任务完成

通过这种方式,代码能够同时启动多个上传任务,并在任务完成后继续处理队列中的分片,从而实现了并发上传的功能。

前端向后端发送请求的方式

主要借助 axios

3.1 检查文件是否已存在(秒传检查)
csharp 复制代码
const checkFileExists = async (hash) => {
  try {
    // 使用 axios 的 get 方法发送请求,传递文件哈希值作为参数
    const response = await axios.get('/checkFile', { params: { hash } });
    // 返回后端响应中的 exists 字段
    return response.data.exists; 
  } catch (error) {
    // 若请求出错,打印错误信息并返回 false
    console.error('检查文件存在性时出错:', error);
    return false;
  }
};

此函数会向后端 /checkFile 接口发送一个 GET 请求,携带文件的哈希值,后端依据该哈希值判断文件是否已存在,随后返回结果。

3.2 获取已上传的切片
csharp 复制代码
const getUploadedChunks = async (hash) => {
  try {
    // 使用 axios 的 get 方法发送请求,传递文件哈希值作为参数
    const response = await axios.get('/getUploadedChunks', { params: { hash } });
    // 返回后端响应中的 chunks 字段
    return response.data.chunks; 
  } catch (error) {
    // 若请求出错,打印错误信息并返回空数组
    console.error('获取已上传切片时出错:', error);
    return [];
  }
};

该函数会向后端 /getUploadedChunks 接口发送一个 GET 请求,携带文件的哈希值,后端返回已上传的切片索引。

3.3 上传单个切片
javascript 复制代码
const uploadChunk = async (chunk, index) => {
  uploadingCount.value++;
  try {
    // 创建一个 FormData 对象,用于存储要上传的数据
    const formData = new FormData();
    // 向 FormData 中添加切片文件
    formData.append('file', chunk);
    // 向 FormData 中添加文件哈希值
    formData.append('hash', fileHash.value);
    // 向 FormData 中添加切片索引
    formData.append('index', index);
    // 使用 axios 的 post 方法发送请求,传递 FormData 对象
    await axios.post('/uploadChunk', formData, {
      onUploadProgress: (progressEvent) => {
        // 计算上传进度百分比
        const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
        // 更新上传进度
        uploadProgress.value = percentCompleted; 
      },
    });
  } catch (error) {
    // 若请求出错,打印错误信息
    console.error('上传切片时出错:', error);
  } finally {
    // 上传完成后,正在上传的任务数量减 1
    uploadingCount.value--;
    if (uploadQueue.value.length > 0) {
      // 若队列中还有待上传的切片,继续上传
      startUploadQueue(); 
    } else if (uploadingCount.value === 0) {
      // 若所有切片上传完成,合并切片
      mergeChunks(); 
    }
  }
};

这个函数会向后端 /uploadChunk 接口发送一个 POST 请求,使用 FormData 携带切片文件、文件哈希值和切片索引。同时,利用 onUploadProgress 事件监听上传进度。

3.4 合并切片
javascript 复制代码
const mergeChunks = async () => {
  try {
    // 使用 axios 的 post 方法发送请求,传递文件哈希值
    await axios.post('/mergeChunks', { hash: fileHash.value });
    // 若合并成功,弹出提示框
    alert('文件上传完成'); 
  } catch (error) {
    // 若请求出错,打印错误信息
    console.error('合并切片时出错:', error);
  }
};

该函数会向后端 /mergeChunks 接口发送一个 POST 请求,携带文件的哈希值,通知后端合并所有切片。

4. 总结

前端向后端发送请求的步骤如下:

  1. 引入 axios 库。
  2. 依据不同的业务需求,使用 axiosgetpost 方法发送请求。
  3. 处理请求的响应和错误。 在实际应用中,根据后端接口的具体要求调整请求参数和请求方式。

大文件上传可以优化的地方

使用webworker计算文件的哈希值

webworker知识点

Web Worker 是一种浏览器提供的 API,允许你在一个独立的线程中执行 JavaScript 代码,与主线程(UI 线程)分离。Web Worker 可以处理计算密集型任务,如数据处理、文件解析等,这些任务通常会阻塞主线程,导致 UI 卡顿。通过 Web Worker,你可以将这些耗时操作移到后台线程,确保主线程始终保持响应状态。

工作原理:

  1. 独立线程:Web Worker 在一个与主线程(UI 线程)分离的线程中运行,主线程和 Worker 线程之间通过消息传递(postMessage)进行通信。
  2. 主线程与 Worker 通信 :主线程可以通过 postMessage() 方法向 Worker 发送数据,Worker 完成计算后,通过 postMessage() 将结果返回给主线程。
  3. 异步操作:由于 Worker 在后台线程中运行,因此它的执行不会阻塞主线程,所有的计算任务都是异步执行的。
  4. 线程间通信 :Worker 无法直接访问主线程的 DOM、window 或者 document 等对象,它只能通过 postMessage() 与主线程进行数据交换。返回的数据是通过事件机制传递的,使用 onmessage 监听数据的返回。

Web Worker 的优势:

  • 性能提升:Web Worker 可以让长时间的计算任务在后台线程中执行,避免 UI 阻塞,提升用户体验。
  • 非阻塞性:主线程可以继续处理用户交互和渲染,而不被复杂计算所阻塞。
  • 多线程处理:对于 CPU 密集型任务,Web Worker 可以将工作分配给多个 Worker,实现并行计算,提高性能。

Web Worker 的应用场景:

  • 大数据处理:例如,处理大量的数组计算、排序、数据筛选等任务。
  • 图像处理:例如,进行图像的处理和转换,而不影响 UI 渲染。
  • 音视频处理:例如,音视频的编码、解码等计算密集型操作。
  • 异步任务:一些需要后台执行的异步任务,可以通过 Worker 来处理。

Web Worker 的局限性:

  • 无法操作 DOM :Web Worker 在独立线程中运行,不能直接访问 DOM 和 window,只能通过消息传递来与主线程交换数据。
  • 数据传递 :数据通过 postMessage() 传递时会发生深拷贝,因此传递大数据时可能会有性能开销。
  • 浏览器支持:大多数现代浏览器支持 Web Worker,但在旧版浏览器中可能不被支持。
  1. 创建一个 Web Worker:

    javascript 复制代码
    // main.js (主线程)
    const worker = new Worker('worker.js') // 创建 Worker 实例
    
    worker.postMessage('Hello, Worker!') // 向 Worker 发送消息
    
    worker.onmessage = function (event) {
      console.log('Worker says: ', event.data) // 接收 Worker 的响应
    }
  2. Worker 文件(worker.js):

    javascript

    javascript 复制代码
    // worker.js (Worker 线程)
    onmessage = function (event) {
      console.log('Main thread says: ', event.data)
      postMessage('Hello, Main Thread!') // 发送响应到主线程
    }

使用webworker计算文件哈希值

js 复制代码
// 计算文件哈希值
const calculateFileHash = (file) => {
  return new Promise((resolve) => {
    const worker = new Worker(new URL('./hashWorker.js', import.meta.url));
    worker.postMessage(file);
    worker.onmessage = (e) => {
      resolve(e.data);
    };
  });
};
js 复制代码
importScripts('https://cdn.jsdelivr.net/npm/[email protected]/spark-md5.min.js');

self.onmessage = (e) => {
  const file = e.data;
  const chunkSize = 1024 * 1024; // 1MB
  const chunks = Math.ceil(file.size / chunkSize);
  let currentChunk = 0;
  const spark = new self.SparkMD5.ArrayBuffer();
  const fileReader = new FileReader();

  fileReader.onload = (e) => {
    spark.append(e.target.result);
    currentChunk++;
    if (currentChunk < chunks) {
      loadNext();
    } else {
      const hash = spark.end();
      self.postMessage(hash);
    }
  };

  fileReader.onerror = () => {
    console.error('读取文件时出错');
  };

  const loadNext = () => {
    const start = currentChunk * chunkSize;
    const end = start + chunkSize >= file.size ? file.size : start + chunkSize;
    fileReader.readAsArrayBuffer(file.slice(start, end));
  };

  loadNext();
};    

参考:JS 基础知识 | 前端面试派 (mianshipai.com)

相关推荐
天天扭码27 分钟前
零基础 | 入门前端必备技巧——使用 DOM 操作插入 HTML 元素
前端·javascript·dom
咖啡虫1 小时前
css中的3d使用:深入理解 CSS Perspective 与 Transform-Style
前端·css·3d
拉不动的猪1 小时前
设计模式之------策略模式
前端·javascript·面试
旭久1 小时前
react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR
前端·javascript·react.js
独行soc1 小时前
2025年常见渗透测试面试题-红队面试宝典下(题目+回答)
linux·运维·服务器·前端·面试·职场和发展·csrf
uhakadotcom2 小时前
Google Earth Engine 机器学习入门:基础知识与实用示例详解
前端·javascript·面试
麓殇⊙2 小时前
Vue--组件练习案例
前端·javascript·vue.js
outstanding木槿2 小时前
React中 点击事件写法 的注意(this、箭头函数)
前端·javascript·react.js
会点php的前端小渣渣2 小时前
vue的计算属性computed的原理和监听属性watch的原理(新)
前端·javascript·vue.js
_一条咸鱼_3 小时前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试