[フロントエンド インタビュー] Zhongda ファイルのアップロード/ダウンロード: 中規模ファイル プロキシ サーバーのリリース + 大規模ファイル スライス転送 + 同時リクエスト + ブレークポイントを達成するローカルストレージでアップロードを再開

目次

中型ファイル プロキシ サーバーのリリース: 10MB 単位

プロキシ

nginx

大きなファイルのスライス: 100MB 単位

ブレークポイント: スライス ハッシュを保存する

フロントエンド ソリューション A

ローカルストレージ

バックエンド ソリューション B

サーバ

アップロード

フロントエンド

後部

ダウンロード

分割ダウンロード: レスポンスヘッダー Content-Range+206 ステータスコード [実際の開発]

フロントエンド

後部

複数の大きなファイル転送:spark-md5

ハッシュ衝突

要約する

Blob.prototype.slice 切片

web-worker は、ワーカー スレッドでspark-md5を使用して、ファイルの内容に基づいてハッシュを計算します。

promise.allSettled() 同時リクエスト


中型ファイル プロキシ サーバーのリリース: 10MB 単位

プロキシ

proxy_bufferingプロキシ バッファリングを有効にするかどうかを制御するには、

proxy_buffer_sizeそしてproxy_buffersバッファのサイズを変更するには

nginx

nginx.conf 構成ファイルで、行う変更の範囲に応じてhttpserverまたはブロックを検索または追加します。このブロックでは、ディレクティブをlocation追加または変更しますclient_max_body_size

http {     ...     サーバー {         ...         場所 /upload {             client_max_body_size 100M;             ...         }         ...     }     ... }










構成ファイルに構文エラーがないか確認してください。

sudo nginx -t

エラーが報告されない場合は、Nginx をリロードして構成の変更を有効にします。

sudo systemctl reload nginx

React バージョンについては、次を参照してください:フロントエンド ファイル ストリーミング、スライスのダウンロードとアップロード: ファイル転送の効率とユーザー エクスペリエンスの最適化 - Nuggets

<pre> タグは、フォーマット済みのテキストを定義します。

<pre> タグの一般的な用途は、コンピュータのソース コードを表すことです。

BLOB (バイナリ ラージ オブジェクト) オブジェクト: バイナリ データを格納します

ArrayBuffer オブジェクト タイプ: キャッシュされたバイナリ データ

大きなファイルのスライス: 100MB 単位

各フラグメントのサイズは通常、数百 KB から数 MB の範囲です。

ブレークポイント: スライス ハッシュを保存する

フロントエンド ソリューション A

ローカルストレージ

  1. サイズ制限: ブラウザごとに制限が異なる場合がありますが、通常、サイズ制限は 5MB ~ 10MB です。ブレークポイントの添字を保存するには十分です

  2. 同一生成元ポリシーに従う

  3. 永続性: ユーザーがブラウザのキャッシュをアクティブにクリアするか、コードを使用してデータを削除した場合にのみ、閉じた後も存在します。

  4. アクセス同期。大量のデータの読み取りまたは書き込み時にブロックされる可能性があります。

  5. データ型: 文字列

  6. 適用可能なシナリオ:小容量、非機密、永続データ。大量のデータを処理する必要がある場合、または異なるドメイン間でデータを共有する必要がある場合は、IndexedDB またはサーバー側ストレージを検討できます。

このようにして、次回のアップロードでは以前にアップロードした部分をスキップできます。メモリ機能を実現するには 2 つの方法があります。

バックエンド ソリューション B

サーバ

フロントエンドのソリューションに欠陥があり、ブラウザを変更するとローカルストレージが無効になるため、後者を推奨します。

アップロード

フロントエンド

<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <button @click="startUpload">Start Upload</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      file: null,
      chunkSize: 1024 * 1024, // 1MB
      totalChunks: 0,
      uploadedChunks: [],
    };
  },
  methods: {
    handleFileChange(event) {
      this.file = event.target.files[0];
    },
    startUpload() {
      if (this.file) {
        this.totalChunks = this.getTotalChunks();
        this.uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
        this.uploadChunks(0);
      }
    },
    uploadChunks(startChunk) {
      if (startChunk >= this.totalChunks) {
        console.log('Upload complete');
        localStorage.removeItem('uploadedChunks'); 
        return;
      }
      //模拟每次至多发起5个并发请求,实际开发中根据请求资源的限定决定?
      const endChunk = Math.min(startChunk + 5, this.totalChunks);

      const uploadPromises = [];
      for (let chunkIndex = startChunk; chunkIndex < endChunk; chunkIndex++) {
        if (!this.uploadedChunks.includes(chunkIndex)) {
          const startByte = chunkIndex * this.chunkSize;
          const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.file.size);
          const chunkData = this.file.slice(startByte, endByte);

          const formData = new FormData();
          formData.append('chunkIndex', chunkIndex);
          formData.append('file', chunkData);

          uploadPromises.push(
            fetch('/upload', {
              method: 'POST',
              body: formData,
            })
          );
        }
      }
      Promise.allSettled(uploadPromises)
        .then(() => {
          const newUploadedChunks = Array.from(
            new Set([...this.uploadedChunks, ...Array.from({ length: endChunk - startChunk }, (_, i) => i + startChunk)])
          );
          this.uploadedChunks = newUploadedChunks;
          localStorage.setItem('uploadedChunks', JSON.stringify(this.uploadedChunks));

          this.uploadChunks(endChunk);
        })
        .catch(error => {
          console.error('Error uploading chunks:', error);
        });
    },
    getTotalChunks() {
      return Math.ceil(this.file.size / this.chunkSize);
    },
  },
};
</script>

後部

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: バイト <開始>-<終了>/<合計>

ステータス コード 206 は、サーバーが一部のリクエストを正常に処理し、対応するデータ範囲を返したことを示します。

Xbox や PlayStation などの多くのプラットフォームのゲーム ファイルも、100 G を超えるファイルをダウンロードするときにこの方法で実装されます。

フロントエンド

<template>
  <div>
    <button @click="startDownload">Start Download</button>
  </div>
</template>

<script>
import { saveAs } from 'file-saver';

export default {
  data() {
    return {
      totalChunks: 0,
      chunkSize: 1024 * 1024, // 默认1M
      fileNm: "file.txt",
      downloadedChunks: [],
      chunks: [], // 存储切片数据
      concurrentDownloads: 5, // 并发下载数量
    };
  },
  methods: {
    startDownload() {
      this.fetchMetadata();
    },
    fetchMetadata() {
      fetch('/metadata')
        .then(response => response.json())
        .then(data => {
          this.totalChunks = data.totalChunks;
          this.chunkSize = data.chunkSize;
          this.fileNm = data.fileNm;
          this.continueDownload();
        })
        .catch(error => {
          console.error('Error fetching metadata:', error);
        });
    },
   async continueDownload() {
      const storedChunks = JSON.parse(localStorage.getItem('downloadedChunks')) || [];
      this.downloadedChunks = storedChunks;

      const downloadPromises = [];
      let chunkIndex = 0;

      while (chunkIndex < this.totalChunks) {
        const chunkPromises = [];
        
        for (let i = 0; i < this.concurrentDownloads; i++) {
          if (chunkIndex < this.totalChunks && !this.downloadedChunks.includes(chunkIndex)) {
            chunkPromises.push(this.downloadChunk(chunkIndex));
          }
          chunkIndex++;
        }

        await Promise.allSettled(chunkPromises);
      }
// 当所有切片都下载完成时 合并切片
      this.mergeChunks();
    },
    
    downloadChunk(chunkIndex) {
      return new Promise((resolve, reject) => {
        const startByte = chunkIndex * this.chunkSize;
        const endByte = Math.min((chunkIndex + 1) * this.chunkSize, this.totalChunks * this.chunkSize);
 //我不太清楚实际开发中切片是靠idx,还是startByte、endByte,还是两者都用....
        fetch(`/download/${chunkIndex}?start=${startByte}&end=${endByte}`)
          .then(response => response.blob())
          .then(chunkBlob => {
            this.downloadedChunks.push(chunkIndex);
            localStorage.setItem('downloadedChunks', JSON.stringify(this.downloadedChunks));

            this.chunks[chunkIndex] = chunkBlob; // 存储切片数据

            resolve();
          })
          .catch(error => {
            console.error('Error downloading chunk:', error);
            reject();
          });
      });
    },
    mergeChunks() {
      const mergedBlob = new Blob(this.chunks);
      // 保存合并后的 Blob 数据到本地文件
      saveAs(mergedBlob, this.fileNm);
      // 清空切片数据和已下载切片的 localStorage
      this.chunks = [];
      localStorage.removeItem('downloadedChunks');
    },
  },
};
</script>

後部

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 (メッセージ ダイジェスト アルゴリズム 5): ハッシュ関数

若使用 文件名 + 切片下标 作为切片 hashこのように、ファイル名を変更すると、その効果は失われます。

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

webpackのcontenthashもこの考え方に基づいて実装されています

また、非常に大きなファイルをアップロードすると、ファイルの内容を読み取ってハッシュを計算するのに非常に時間がかかり、ページがフリーズする可能性があることを考慮して、 Web-workerを使用してハッシュを計算します。ワーカー スレッドにより、ユーザーは引き続きメイン インターフェイスで通常どおり作業できるようになります。引起 UI 的阻塞

// /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. スライスハッシュ/送信およびその他の目的のため

  2. メモリ効率:大きなファイルの場合、ファイル全体を一度にメモリにロードすると、過剰なメモリ使用量が発生したり、ブラウザがクラッシュしたりする可能性があります。ファイルを小さなチャンクに分割することで、処理中に操作する必要があるチャンクは 1 つだけになり、メモリの負荷が軽減されます。

  3. パフォーマンスの最適化:ファイル全体をハッシュ関数に直接渡すと、特に大きなファイルの場合、計算時間が長くなる可能性があります。ハッシュ値を小さなブロックに分割し、1つずつハッシュ値を計算することで、複数のブロックを並列処理することができ、計算効率が向上します。

  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 ドキュメントでは、すべてのスライスが渡され、ハッシュ値が計算される必要があります。ファイル全体を直接計算に含めることはできません。そうしないと、異なるファイルであっても同じハッシュを持つことになります。

要約する

Blob.prototype.slice 切片

web-worker は、ワーカー スレッドでspark-md5を使用して、ファイルの内容に基づいてハッシュを計算します。

約束。allSettled()并发请求

インタビュアーの Jie Jie は微笑みました。「大きなファイルのアップロード機能を使用したことがありませんか?」その後、戻って通知を待ちます。- ナゲット

おすすめ

転載: blog.csdn.net/qq_28838891/article/details/132358193
おすすめ