面試常考--前端效能最佳化之大檔案上傳

最小生成树發表於2024-07-24

大檔案上傳是前端開發中常見的需求之一,特別是在需要處理高畫質圖片、影片或其他大型檔案時。最佳化大檔案上傳不僅可以提升使用者體驗,還能有效減輕伺服器負擔。本文將深入探討大檔案上傳的幾種常見最佳化技術,包括檔案切片與併發上傳、斷點續傳、後臺處理最佳化、安全性考慮和使用者體驗最佳化。

1. 前言

在現代Web應用中,使用者上傳大檔案已成為常見需求。然而,直接上傳大檔案會面臨諸多挑戰,例如網路不穩定導致上傳中斷、長時間上傳導致使用者體驗差、伺服器壓力大等。因此,最佳化大檔案上傳效能顯得尤為重要。

2. 檔案切片與併發上傳

2.1 檔案切片原理

檔案切片(Chunking)是將大檔案分成若干小片段,每個片段獨立上傳的方法。這樣做可以有效減少單次上傳的資料量,降低上傳失敗的機率。

2.2 實現步驟

  1. 前端切片:利用Blob物件的slice方法將檔案切片。
  2. 併發上傳:使用Promise.all實現多個切片併發上傳。
  3. 合併請求:上傳完成後,通知伺服器合併這些切片。

3. 斷點續傳

斷點續傳(Resumable Uploads)可以在上傳過程中斷時,從斷點繼續上傳,避免重新上傳整個檔案。

3.1 實現步驟

  1. 前端記錄進度:使用localStorage記錄已上傳的切片資訊。
  2. 斷點續傳:上傳時檢查哪些切片未上傳,繼續上傳未完成的部分。

4. 後臺處理最佳化

4.1 分片接收與合併

伺服器需要支援接收分片請求,並在所有分片上傳完成後合併檔案。可以利用中介軟體或服務端程式語言實現這一邏輯。

5. 安全性考慮

5.1 檔案型別校驗

在前端和後端都應對檔案型別進行校驗,確保上傳的檔案型別符合預期。

5.2 檔案大小限制

限制單個檔案和總上傳檔案的大小,防止惡意使用者上傳過大的檔案造成伺服器壓力。

6. 使用者體驗最佳化

6.1 進度顯示

透過顯示上傳進度條,讓使用者瞭解上傳進度,提升使用者體驗。

6.2 網路波動處理

考慮到使用者可能在網路不穩定的環境中上傳檔案,可以增加失敗重試機制。

完整例項

後端程式碼(Node.js + Express)

安裝依賴

npm init -y
npm install express multer fs

建立伺服器檔案(server.js)

const express = require('express');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const bodyParser = require('body-parser');

const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(bodyParser.json());

// 路由:處理檔案切片上傳
app.post('/upload', upload.single('chunk'), (req, res) => {
  const { index, fileName } = req.body;
  const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${index}`);
  fs.renameSync(req.file.path, chunkPath);
  res.status(200).send('Chunk uploaded');
});

// 路由:合併切片
app.post('/merge', (req, res) => {
  const { totalChunks, fileName } = req.body;
  const filePath = path.join(__dirname, 'uploads', fileName);
  const writeStream = fs.createWriteStream(filePath);

  for (let i = 0; i < totalChunks; i++) {
    const chunkPath = path.join(__dirname, 'uploads', `${fileName}-${i}`);
    const data = fs.readFileSync(chunkPath);
    writeStream.write(data);
    fs.unlinkSync(chunkPath);
  }

  writeStream.end();
  res.status(200).send('File merged');
});

app.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});

前端程式碼(index.html + script.js)

  1. 建立HTML檔案(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>大檔案上傳</title>
</head>
<body>
  <input type="file" id="fileInput">
  <progress id="progressBar" value="0" max="100"></progress>
  <button onclick="uploadFile()">上傳檔案</button>
  <script src="script.js"></script>
</body>
</html>

  1. 建立JavaScript檔案(script.js)
const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');
const chunkSize = 5 * 1024 * 1024; // 5MB

const uploadChunk = async (chunk, index, fileName) => {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('index', index);
  formData.append('fileName', fileName);

  await fetch('/upload', {
    method: 'POST',
    body: formData
  });

  updateProgressBar(index);
};

const updateProgressBar = (index) => {
  const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
  if (!uploadedChunks.includes(index)) {
    uploadedChunks.push(index);
    progressBar.value = (uploadedChunks.length / totalChunks) * 100;
    localStorage.setItem('uploadedChunks', JSON.stringify(uploadedChunks));
  }
};

const uploadFile = async () => {
  const file = fileInput.files[0];
  const totalChunks = Math.ceil(file.size / chunkSize);
  const uploadedChunks = JSON.parse(localStorage.getItem('uploadedChunks')) || [];
  const promises = [];

  for (let i = 0; i < totalChunks; i++) {
    if (!uploadedChunks.includes(i)) {
      const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
      promises.push(uploadChunk(chunk, i, file.name));
    }
  }

  await Promise.all(promises);

  await fetch('/merge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ totalChunks, fileName: file.name })
  });

  localStorage.removeItem('uploadedChunks');
  alert('檔案上傳成功');
};

啟動後端伺服器

  1. 在瀏覽器中開啟前端頁面

index.html檔案在瀏覽器中開啟,選擇檔案並點選“上傳檔案”按鈕即可看到檔案上傳進度。

node server.js

相關文章