搜尋
首頁頭條大檔案怎麼快速上傳?來看看我的實作方法!

大檔案快速上傳的方案,相信你也有過了解,其實無非就是將 檔案變小,也就是透過 壓縮檔案資源 或是 檔案資源分塊 後上傳。

本文只介紹資源分塊上傳的方式,並且會透過 前端(vue3 vite) 與 服務端(nodejs koa2) 互動的方式,實現大檔案分塊上傳的簡單功能。

整理想法


#問題 1:誰負責資源分塊?誰負責資源整合?

當然這個問題也很簡單,一定是前端負責分塊,服務端負責整合。

問題 2:前端怎麼對資源進行分塊?

首先是選擇上傳的檔案資源,接著就可以得到對應的檔案物件 File,而 File.prototype.slice 方法可以實現資源的分塊,當然也有人說是 Blob.prototype. slice 方法,因為 Blob.prototype.slice === File.prototype.slice

問題 3:服務端怎麼知道什麼時候要整合資源?如何保證資源整合的有序性?

由於前端會將資源分塊,然後單獨發送請求,也就是說,原來1 個檔案對應1 個上傳請求,現在可能會變成1 個檔案對應n 個上傳請求,所以前端可以基於Promise.all 將這多個介面整合,上傳完成在發送一個合併的請求,通知服務端進行合併。

合併時可透過 nodejs 中的讀寫流(readStream/writeStream),將所有切片的流透過管道(pipe)輸入最終檔案的流中。

在傳送請求資源時,前端會定好每個檔案對應的序號,並將目前分塊、序號以及檔案hash 等資訊一起傳送給服務端,服務端在進行合併時,透過序號進行依序合併即可。

問題 4:如果某個分塊的上傳請求失敗了,怎麼辦?

一旦服務端某個上傳請求失敗,會傳回目前分塊失敗的訊息,其中會包含檔案名稱、檔案hash、分塊大小以及分塊序號等,前端拿到這些訊息後可以進行重傳,同時考慮此時是否需要將Promise.all 替換為Promise.allSettled 更方便。

前端部分


#建立專案

透過pnpm create vite建立項目,對應文件目錄如下.

大檔案怎麼快速上傳?來看看我的實作方法!

請求模組

src/request.js

該文件就是針對axios 進行簡單的封裝,如下:

import axios from "axios";
const baseURL = 'http://localhost:3001';
export const uploadFile = (url, formData, onUploadProgress = () => { }) => {
  return axios({
    method: 'post',
    url,
    baseURL,
    headers: {
      'Content-Type': 'multipart/form-data'
    },
    data: formData,
    onUploadProgress
  });
}
export const mergeChunks = (url, data) => {
  return axios({
    method: 'post',
    url,
    baseURL,
    headers: {
      'Content-Type': 'application/json'
    },
    data
  });
}

檔案資源分塊

根據DefualtChunkSize = 5 * 1024 * 1024 ,即5 MB ,來對檔案進行資源分塊進行計算,透過spark-md5[1] 根據檔案內容計算出檔案的hash 值,方便做其他最佳化,例如:當hash 值不變時,服務端沒有必要重複讀寫檔案等。

// 获取文件分块
const getFileChunk = (file, chunkSize = DefualtChunkSize) => {
  return new Promise((resovle) => {
    let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,
      chunks = Math.ceil(file.size / chunkSize),
      currentChunk = 0,
      spark = new SparkMD5.ArrayBuffer(),
      fileReader = new FileReader();
    fileReader.onload = function (e) {
      console.log('read chunk nr', currentChunk + 1, 'of');
      const chunk = e.target.result;
      spark.append(chunk);
      currentChunk++;
      if (currentChunk < chunks) {
        loadNext();
      } else {
        let fileHash = spark.end();
        console.info(&#39;finished computed hash&#39;, fileHash);
        resovle({ fileHash });
      }
    };
    fileReader.onerror = function () {
      console.warn(&#39;oops, something went wrong.&#39;);
    };
    function loadNext() {
      let start = currentChunk * chunkSize,
        end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
      let chunk = blobSlice.call(file, start, end);
      fileChunkList.value.push({ chunk, size: chunk.size, name: currFile.value.name });
      fileReader.readAsArrayBuffer(chunk);
    }
    loadNext();
  });
}

發送上傳請求和合併請求

透過Promise.all 方法整合所以分塊的上傳請求,在所有分塊資源上傳完畢後,在then 中發送合併請求。

// 上传请求
const uploadChunks = (fileHash) => {
  const requests = fileChunkList.value.map((item, index) => {
    const formData = new FormData();
    formData.append(`${currFile.value.name}-${fileHash}-${index}`, item.chunk);
    formData.append("filename", currFile.value.name);
    formData.append("hash", `${fileHash}-${index}`);
    formData.append("fileHash", fileHash);
    return uploadFile(&#39;/upload&#39;, formData, onUploadProgress(item));
  });
  Promise.all(requests).then(() => {
    mergeChunks(&#39;/mergeChunks&#39;, { size: DefualtChunkSize, filename: currFile.value.name });
  });
}

進度條資料

分塊進度資料利用axios 中的onUploadProgress 設定項取得數據,透過使用computed 根據分塊進度資料的變更自動自動計算當前文件的總進度。

// 总进度条
const totalPercentage = computed(() => {
  if (!fileChunkList.value.length) return 0;
  const loaded = fileChunkList.value
    .map(item => item.size * item.percentage)
    .reduce((curr, next) => curr + next);
  return parseInt((loaded / currFile.value.size).toFixed(2));
})
// 分块进度条
const onUploadProgress = (item) => (e) => {
  item.percentage = parseInt(String((e.loaded / e.total) * 100));
}

服務端部分


#建置服務

  • #使用koa2 建構簡單的服務,連接埠為3001
  • #使用koa-body 處理接收前端傳遞

    'Content-Type': 'multipart/form-data'
  • 類型的資料
  • 使用koa-router 註冊服務端路由
  • 使用koa2-cors 處理跨網域問題

#目錄/檔案分割

server/server.js

該檔案是服務端特定的程式碼實現,用於處理接收和整合分塊資源。

server/resources

    此目錄是用來存放單一檔案的多個分塊,以及最後分塊整合後的資源:
  • #分塊資源未合併時,會在該目錄下以目前檔案名稱建立一個目錄,用於存放這個該檔案相關的所有分塊
  • ###分當區塊資源需合併時,會讀取這個檔案對應的目錄下的所有分塊資源,然後將它們整合成原始檔案###
  • 分块资源合并完成,会删除这个对应的文件目录,只保留合并后的原文件,生成的文件名比真实文件名多一个 _ 前缀,如原文件名 "测试文件.txt" 对应合并后的文件名 "_测试文件.txt"

接收分块

使用 koa-body 中的 formidable 配置中的 onFileBegin 函数处理前端传来的 FormData 中的文件资源,在前端处理对应分块名时的格式为:filename-fileHash-index,所以这里直接将分块名拆分即可获得对应的信息。

// 上传请求
router.post(
  &#39;/upload&#39;,
  // 处理文件 form-data 数据
  koaBody({
    multipart: true,
    formidable: {
      uploadDir: outputPath,
      onFileBegin: (name, file) => {
        const [filename, fileHash, index] = name.split(&#39;-&#39;);
        const dir = path.join(outputPath, filename);
        // 保存当前 chunk 信息,发生错误时进行返回
        currChunk = {
          filename,
          fileHash,
          index
        };
        // 检查文件夹是否存在如果不存在则新建文件夹
        if (!fs.existsSync(dir)) {
          fs.mkdirSync(dir);
        }
        // 覆盖文件存放的完整路径
        file.path = `${dir}/${fileHash}-${index}`;
      },
      onError: (error) => {
        app.status = 400;
        app.body = { code: 400, msg: "上传失败", data: currChunk };
        return;
      },
    },
  }),
  // 处理响应
  async (ctx) => {
    ctx.set("Content-Type", "application/json");
    ctx.body = JSON.stringify({
      code: 2000,
      message: &#39;upload successfully!&#39;
    });
  });

整合分块

通过文件名找到对应文件分块目录,使用 fs.readdirSync(chunkDir) 方法获取对应目录下所以分块的命名,在通过 fs.createWriteStream/fs.createReadStream 创建可写/可读流,结合管道 pipe 将流整合在同一文件中,合并完成后通过 fs.rmdirSync(chunkDir) 删除对应分块目录。

// 合并请求
router.post(&#39;/mergeChunks&#39;, async (ctx) => {
  const { filename, size } = ctx.request.body;
  // 合并 chunks
  await mergeFileChunk(path.join(outputPath, &#39;_&#39; + filename), filename, size);
  // 处理响应
  ctx.set("Content-Type", "application/json");
  ctx.body = JSON.stringify({
    data: {
      code: 2000,
      filename,
      size
    },
    message: &#39;merge chunks successful!&#39;
  });
});
// 通过管道处理流 
const pipeStream = (path, writeStream) => {
  return new Promise(resolve => {
    const readStream = fs.createReadStream(path);
    readStream.pipe(writeStream);
    readStream.on("end", () => {
      fs.unlinkSync(path);
      resolve();
    });
  });
}
// 合并切片
const mergeFileChunk = async (filePath, filename, size) => {
  const chunkDir = path.join(outputPath, filename);
  const chunkPaths = fs.readdirSync(chunkDir);
  if (!chunkPaths.length) return;
  // 根据切片下标进行排序,否则直接读取目录的获得的顺序可能会错乱
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
  console.log("chunkPaths = ", chunkPaths);
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
        // 指定位置创建可写流
        fs.createWriteStream(filePath, {
          start: index * size,
          end: (index + 1) * size
        })
      )
    )
  );
  // 合并后删除保存切片的目录
  fs.rmdirSync(chunkDir);
};

前端 & 服务端 交互


前端分块上传

测试文件信息:

大檔案怎麼快速上傳?來看看我的實作方法!

选择文件类型为 19.8MB,而且上面设定默认分块大小为 5MB ,于是应该要分成 4 个分块,即 4 个请求。

大檔案怎麼快速上傳?來看看我的實作方法!

服务端分块接收

大檔案怎麼快速上傳?來看看我的實作方法!

前端发送合并请求

大檔案怎麼快速上傳?來看看我的實作方法!

服务端合并分块

大檔案怎麼快速上傳?來看看我的實作方法!

扩展 —— 断点续传 & 秒传


有了上面的核心逻辑之后,要实现断点续传和秒传的功能,只需要在取扩展即可,这里不再给出具体实现,只列出一些思路。

断点续传

断点续传其实就是让请求可中断,然后在接着上次中断的位置继续发送,此时要保存每个请求的实例对象,以便后期取消对应请求,并将取消的请求保存或者记录原始分块列表取消位置信息等,以便后期重新发起请求。

取消请求的几种方式:

  • 如果使用原生 XHR 可使用 (new XMLHttpRequest()).abort() 取消请求

  • 如果使用 axios 可使用 new CancelToken(function (cancel) {}) 取消请求

  • 如果使用 fetch 可使用 (new AbortController()).abort() 取消请求

秒传

不要被这个名字给误导了,其实所谓的秒传就是不用传,在正式发起上传请求时,先发起一个检查请求,这个请求会携带对应的文件 hash 给服务端,服务端负责查找是否存在一模一样的文件 hash,如果存在此时直接复用这个文件资源即可,不需要前端在发起额外的上传请求。

最后


前端分片上传的内容单纯从理论上来看其实还是容易理解的,但是实际自己去实现的时候还是会踩一些坑,比如服务端接收解析 formData 格式的数据时,没法获取文件的二进制数据等。

更多编程相关知识,请访问:编程视频!!

陳述
本文轉載於:weixin。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

mPDF

mPDF

mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境