【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传

目录

中等文件代理服务器放行:10MB为单位

proxy

nginx

大文件切片:100MB为单位

断点:存储切片hash

前端方案A

localstorage

后端方案B

服务端

上传

前端

后端

下载

分段下载:响应头Content-Range+206状态码【实际开发】

前端

后端

多个大文件传输:spark-md5

哈希碰撞

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求


中等文件代理服务器放行:10MB为单位

proxy

proxy_buffering来控制是否启用代理缓冲,

proxy_buffer_sizeproxy_buffers来调整缓冲区的大小

nginx

在nginx.conf配置文件中,找到或添加一个 httpserverlocation 块,具体位置取决于希望修改的范围。在该块中,添加或修改 client_max_body_size 指令

http {
    ...
    server {
        ...
        location /upload {
            client_max_body_size 100M;
            ...
        }
        ...
    }
    ...
}

检查配置文件是否有语法错误:

sudo nginx -t

如果没有报告错误,重新加载Nginx以使配置更改生效:

sudo systemctl reload nginx

【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传_第1张图片

React版本见:前端文件流、切片下载和上传:优化文件传输效率与用户体验 - 掘金

 标签可定义预格式化的文本。

 标签的一个常见应用就是用来表示计算机的源代码

Blob(Binary Large Object)对象:存储二进制数据

ArrayBuffer 对象类型:缓存二进制数据

大文件切片:100MB为单位

【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传_第2张图片

每个片段大小通常在几百KB到几MB之间

【前端面试】中大文件上传/下载:中等文件代理服务器放行+大文件切片传输+并发请求+localstorage实现断点续传_第3张图片

断点:存储切片hash

前端方案A

localstorage

  1. 容量限制: 不同浏览器可能有不同的限制,但通常容量限制在 5MB 到 10MB 之间。用于存储断点下标够用

  2. 遵循同源策略

  3. 持久性: 关闭后也存在,只有用户主动清除浏览器缓存或使用代码删除数据,

  4. 访问同步,在读取或写入大量数据时,可能阻塞

  5. 数据类型: string

  6. 适用场景:容量小,非敏感,持久性数据。如果需要处理更大容量的数据,或者需要在不同域之间共享数据,可以考虑 IndexedDB 或服务器端存储。

这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

后端方案B

服务端

前端方案有一个缺陷,如果换了个浏览器就localstorage就失效了,所以推荐后者

上传

前端



后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const multer = require('multer');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');

app.use(express.json());
app.use(express.static(chunkDirectory));

const storage = multer.diskStorage({
  destination: chunkDirectory,
  filename: (req, file, callback) => {
    callback(null, `chunk_${req.body.chunkIndex}`);
  },
});

const upload = multer({ storage });

app.post('/upload', upload.single('file'), (req, res) => {
  const { chunkIndex } = req.body;
  console.log(`Uploaded chunk ${chunkIndex}`);
  res.sendStatus(200);
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

下载

分段下载:响应头Content-Range+206状态码【实际开发】

Content-Range: bytes -/

206状态码,表示服务器成功处理了部分请求,并返回了相应的数据范围。

很多平台的游戏文件,比如 Xbox、PlayStation 下载超过 100 G 的文件的时候,也是这样实现的。

前端




后端

const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const chunkDirectory = path.join(__dirname, 'chunks');

app.use(express.json());

app.get('/metadata', (req, res) => {
  const filePath = path.join(__dirname, 'file.txt'); 
  const chunkSize = 1024 * 1024; // 1MB
  const fileNm='file.txt';
  const fileStats = fs.statSync(filePath);
  const totalChunks = Math.ceil(fileStats.size / chunkSize);
  res.json({ totalChunks, chunkSize, fileNm });
});

app.get('/download/:chunkIndex', (req, res) => {
  const chunkIndex = parseInt(req.params.chunkIndex);
  const chunkSize = 1024 * 1024; // 1MB
  const startByte = chunkIndex * chunkSize;
  const endByte = (chunkIndex + 1) * chunkSize;

  const filePath = path.join(__dirname, 'file.txt'); 

  fs.readFile(filePath, (err, data) => {
    if (err) {
      res.status(500).send('Error reading file.');
    } else {
      const chunkData = data.slice(startByte, endByte);
      res.send(chunkData);
    }
  });
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

多个大文件传输:spark-md5

MD5(Message Digest Algorithm 5):哈希函数

若使用 文件名 + 切片下标 作为切片 hash,这样做文件名一旦修改就失去了效果,

所以应该用spark-md5根据文件内容生成 hash

webpack 的contenthash 也是基于这个思路实现的

另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

// /public/hash.js

// 导入脚本
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;

  // 递归加载下一个文件块
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);

      // 检查是否处理完所有文件块
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        // 更新进度百分比并发送消息
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });

        // 递归调用以加载下一个文件块
        loadNext(count);
      }
    };
  };

  // 开始加载第一个文件块
  loadNext(0);
};
  1. 切片hash/传输等目的都是为了

  2. 内存效率: 对于大文件,一次性将整个文件加载到内存中可能会导致内存占用过高,甚至造成浏览器崩溃。通过将文件切成小块,在处理过程中只需要操作单个块,减小了内存的压力。

  3. 性能优化: 如果直接将整个文件传递给哈希函数,可能会导致计算时间较长,尤其是对于大文件。分成小块逐个计算哈希值,可以并行处理多个块,提高计算效率。

  4. 错误恢复: 在上传或下载过程中,网络中断或其他错误可能会导致部分文件块没有传输成功。通过分块计算哈希,你可以轻松检测到哪些块没有正确传输,从而有机会恢复或重新传输这些块。

  5. // 生成文件 hash(web-worker)
    calculateHash(fileChunkList) {
      return new Promise(resolve => {
        // 创建一个新的 Web Worker,并加载指向 "hash.js" 的脚本
        this.container.worker = new Worker("/hash.js");
    
        // 向 Web Worker 发送文件块列表
        this.container.worker.postMessage({ fileChunkList });
    
        // 当 Web Worker 发送消息回来时触发的事件处理程序
        this.container.worker.onmessage = e => {
          const { percentage, hash } = e.data;
    
          // 更新 hash 计算进度
          this.hashPercentage = percentage;
    
          if (hash) {
            // 如果计算完成,解析最终的 hash 值
            resolve(hash);
          }
        };
      });
    },
    
    // 处理文件上传的函数
    async handleUpload() {
      if (!this.container.file) return;
    
      // 将文件划分为文件块列表
      const fileChunkList = this.createFileChunk(this.container.file);
    
      // 计算文件 hash,并将结果存储在容器中
      this.container.hash = await this.calculateHash(fileChunkList);
    
      // 根据文件块列表创建上传数据对象
      this.data = fileChunkList.map(({ file, index }) => ({
        fileHash: this.container.hash,
        chunk: file,
        hash: this.container.file.name + "-" + index,
        percentage: 0
      }));
    
      // 上传文件块
      await this.uploadChunks();
    }
    

哈希碰撞

输入空间通常大于输出空间,无法完全避免碰撞

哈希(A) = 21 % 10 = 1

哈希(B) = 31 % 10 = 1

所以spark-md5 文档中要求传入所有切片并算出 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash

总结

Blob.prototype.slice 切片

web-worker 在 worker 线程中用spark-md5 根据文件内容算hash

promise.allSettled()并发请求

面试官桀桀一笑:你没做过大文件上传功能?那你回去等通知吧! - 掘金

你可能感兴趣的:(前端面试,开发,前端)