JavaScript >> Javascript 文檔 >  >> Node.js

如何使用 Node.js 和 JavaScript 創建和下載 Zip 文件

如何在 Node.js 中創建和填充 zip 存檔,然後使用 JavaScript 在瀏覽器中下載它。

開始使用

對於本教程,我們將使用 CheatCode Node.js Server Boilerplate 以及 CheatCode Next.js Boilerplate。現在讓我們克隆其中的每一個並安裝我們需要的依賴項。

從服務器開始:

終端

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

接下來,安裝服務器樣板的內置依賴項:

終端

cd nodejs-server-boilerplate && npm install

完成後,添加 jszip 我們將用於生成 zip 存檔的依賴項:

終端

npm install jszip

有了這個集合,接下來,讓我們為前端克隆 Next.js 樣板:

終端

git clone https://github.com/cheatcode/nextjs-boilerplate.git

同樣,讓我們安裝依賴項:

終端

cd nextjs-boilerplate && npm install

現在,讓我們添加 b64-to-blobfile-saver 我們在客戶端上需要的依賴項:

終端

npm i b64-to-blob file-saver

現在,在終端的單獨選項卡/窗口中,讓我們啟動服務器和客戶端(兩者都使用克隆目錄根目錄中的相同命令-nodejs-server-boilerplatenextjs-boilerplate ):

終端

npm run dev

添加一個端點,我們將在其中檢索我們的 zip 存檔

首先,讓我們在服務器中連接一個新的 Express.js 端點,我們可以從客戶端調用它來觸發我們的 zip 存檔的下載:

/api/index.js

import graphql from "./graphql/server";
import generateZipForPath from "../lib/generateZipForPath";

export default (app) => {
  graphql(app);

  app.use("/zip", async (req, res) => {
    const zip = await generateZipForPath("lib");
    res.send(zip);
  });
};

很簡單。在這裡,我們只需要一個簡單的路由,我們可以將其用作“遠程控制”來觸發我們的 zip 存檔的下載並將其內容返回給客戶端上的我們。在這裡,我們使用主 API index.js 包含在 Node.js 服務器樣板文件中的文件(只不過是一個用於組織代碼的包裝函數——這裡沒有特殊約定)。

為此,我們在 Express app 上創建一條新路線 (通過 /index.js 傳遞給我們 app.use() 的樣板根目錄下的文件) , 傳遞 /zip 對於我們將調用的 URL。接下來,在路由的回調中,我們調用接下來要構建的函數——generateZipForPath() ——傳遞服務器上我們要“壓縮”的目錄。在這種情況下,我們將只使用 /lib 以服務器根目錄為例。

接下來,讓我們獲取 generateZipForPath() 設置並學習如何填充我們的 zip。

使用 JSZip 創建 zip 存檔

我們將展示兩種將文件添加到 zip 的方法:一次一個文件以及添加目錄的全部內容(包括其子文件夾)。首先,讓我們設置我們的基本 zip 存檔並看看如何添加單個文件:

/lib/generateZipForPath.js

import JSZip from "jszip";

export default (directoryPath = "") => {
  const zip = new JSZip();

  zip.file(
    "standalone.txt",
    "I will exist inside of the zip archive, but I'm not a real file here on the server."
  );
  
  // We'll add more files and finalize our zip here.
};

在這裡,我們定義並導出位於上一節中預期路徑的函數。在這裡,我們的函數接受一個 directoryPath 參數指定我們要添加到 zip 中的文件夾的路徑(這將在下一步中派上用場)。

在函數體中,我們使用 new JSZip() 啟動新的 zip 存檔 .就像看起來一樣,這會在內存中為我們創建一個新的 zip 存檔。

在此下方,我們調用 zip.file() 將我們要添加的文件的名稱傳遞給它,然後是我們要放置在該文件中的內容。這很重要。

這裡的核心思想是我們在內存中創建一個 zip 文件 .我們不是 將 zip 文件寫入磁盤(不過,如果您願意,可以使用 fs.writeFileSync() — 請參閱下面的“轉換 zip 數據”步驟以獲取有關如何執行此操作的提示)。

當我們調用 zip.file() 我們說的是“在內存中創建一個文件,然後用這些內容在內存中填充該文件。”換句話說,這個文件——從技術上講——不存在。我們正在動態生成它。

/lib/generateZipForPath.js

import fs from "fs";
import JSZip from "jszip";

const addFilesFromDirectoryToZip = (directoryPath = "", zip) => {
  const directoryContents = fs.readdirSync(directoryPath, {
    withFileTypes: true,
  });
 
  directoryContents.forEach(({ name }) => {
    const path = `${directoryPath}/${name}`;

    if (fs.statSync(path).isFile()) {
      zip.file(path, fs.readFileSync(path, "utf-8"));
    }

    if (fs.statSync(path).isDirectory()) {
      addFilesFromDirectoryToZip(path, zip);
    }
  });
};

export default async (directoryPath = "") => {
  const zip = new JSZip();

  zip.file(
    "standalone.txt",
    "I will exist inside of the zip archive, but I'm not a real file here on the server."
  );

  addFilesFromDirectoryToZip(directoryPath, zip);

  // We'll finalize our zip archive here...
};

現在是棘手的部分。請記住,我們想學習如何添加單個文件(我們剛剛在上面完成的)以及如何添加目錄。在這裡,我們引入了對新函數 addFilesFromDirectoryToZip() 的調用 將 directoryPath 傳遞給它 我們之前提到的參數以及我們的 zip 實例(我們不完整的 zip 存檔)。

/lib/generateZipForPath.js

import fs from "fs";
import JSZip from "jszip";

const addFilesFromDirectoryToZip = (directoryPath = "", zip) => {
  const directoryContents = fs.readdirSync(directoryPath, {
    withFileTypes: true,
  });
 
  directoryContents.forEach(({ name }) => {
    const path = `${directoryPath}/${name}`;

    if (fs.statSync(path).isFile()) {
      zip.file(path, fs.readFileSync(path, "utf-8"));
    }

    if (fs.statSync(path).isDirectory()) {
      addFilesFromDirectoryToZip(path, zip);
    }
  });
};

export default async (directoryPath = "") => {
  [...]

  addFilesFromDirectoryToZip(directoryPath, zip);

  // We'll finalize our zip archive here...
};

關注那個函數,我們可以看到它接受了我們期望的兩個參數:directoryPathzip .

在函數體內,我們調用 fs.readdirSync() , 傳入給定的 directoryPath 說“去給我們這個目錄中的文件列表”確保添加 withFileTypes: true 這樣我們就有了每個文件的完整路徑。

接下來,期待 directoryContents 包含一個或多個文件的數組(作為具有 name 的對象返回 表示當前被循環的文​​件名的屬性),我們使用 .forEach() 遍歷每個找到的文件,解構 name 屬性(想想這就像從一串葡萄中摘下一顆葡萄,而這串是我們當前循環的對象)。

用那個 name 屬性,我們構造文件的路徑,連接 directoryPath 我們傳入 addFilesFromDirectoryToZip()name .接下來使用這個,我們執行兩個檢查中的第一個,看看我們當前循環的路徑是否是一個文件。

如果是,我們將該文件添加到我們的 zip 中,就像我們之前看到的 zip.file() .不過,這一次,我們傳入 path 作為文件名(當我們這樣做時,JSZip 會自動創建任何嵌套的目錄結構)然後我們使用 fs.readFileSync() 去閱讀文件的內容。同樣,我們說的是“在內存中存在的 zip 文件中的這個路徑上,用我們正在讀取的文件的內容填充它。”

接下來,我們執行第二次檢查,看看我們當前循環的文件是否不是文件,而是目錄。如果是,我們遞歸地 調用addFilesFromDirectoryToZip() , 傳入 path 我們生成和我們現有的 zip 實例。

這可能會令人困惑。遞歸是一個編程概念,它本質上描述了“做某事直到它不能做其他任何事情”的代碼。

在這裡,因為我們正在遍歷目錄,所以我們說“如果您正在循環的文件是一個文件,請將其添加到我們的 zip 並繼續前進。但是,如果您正在循環的文件是一個目錄,請調用再次調用這個函數,傳入當前路徑作為起點,然後循環遍歷that 目錄的文件,將每個文件添加到指定路徑的 zip 中。”

因為我們使用的是 sync fs.readdir 的版本 , fs.stat , 和 fs.readFile ,這個遞歸循環將一直運行,直到沒有更多的子目錄可以遍歷。這意味著一旦完成,我們的函數將“解除阻塞” JavaScript 事件循環並繼續執行 generateZipForPath() 的其餘部分 功能。

將 zip 數據轉換為 base64

現在我們的 zip 包含了我們想要的所有文件和文件夾,讓我們把那個 zip 轉換成一個我們可以輕鬆發送回客戶端的 base64 字符串。

/lib/generateZipForPath.js

import fs from "fs";
import JSZip from "jszip";

const addFilesFromDirectoryToZip = (directoryPath = "", zip) => {
  [...]
};

export default async (directoryPath = "") => {
  const zip = new JSZip();

  zip.file(
    "standalone.txt",
    "I will exist inside of the zip archive, but I'm not a real file here on the server."
  );

  addFilesFromDirectoryToZip(directoryPath, zip);

  const zipAsBase64 = await zip.generateAsync({ type: "base64" });

  return zipAsBase64;
};

服務器上的最後一步。使用我們的 zip 完成,現在我們將導出的函數更新為使用 async 關鍵字,然後調用 await zip.generateAsnyc() 傳遞 { type: 'base64' } 以表示我們想要以 base64 字符串格式取回我們的 zip 文件。

await 這裡只是一個語法技巧(也稱為“語法糖”)來幫助我們避免鏈接 .then() 回調我們對 zip.generateAsync() 的調用 .此外,這使我們的異步代碼以同步樣式格式讀取(這意味著,JavaScript 允許每行代碼在移動到下一行之前完成並返回)。所以,在這裡,我們“等待”調用 zip.generateAsync() 的結果 只有當它完成時,我們才 return 我們期望從該函數返回的值 zipAsBase64 .

服務端就是這樣,接下來,讓我們跳到客戶端,看看如何將它下載到我們的計算機上。

在客戶端設置下載

這部分稍微容易一些。讓我們做一個代碼轉儲,然後單步執行:

/pages/zip/index.js

import React, { useState } from "react";
import b64ToBlob from "b64-to-blob";
import fileSaver from "file-saver";

const Zip = () => {
  const [downloading, setDownloading] = useState(false);

  const handleDownloadZip = () => {
    setDownloading(true);

    fetch("http://localhost:5001/zip")
      .then((response) => {
        return response.text();
      })
      .then((zipAsBase64) => {
        const blob = b64ToBlob(zipAsBase64, "application/zip");
        fileSaver.saveAs(blob, `example.zip`);
        setDownloading(false);
      });
  };

  return (
    <div>
      <h4 className="mb-5">Zip Downloader</h4>
      <button
        className="btn btn-primary"
        disabled={downloading}
        onClick={handleDownloadZip}
      >
        {downloading ? "Downloading..." : "Download Zip"}
      </button>
    </div>
  );
};

Zip.propTypes = {};

export default Zip;

在這裡,我們創建了一個虛擬的 React 組件 Zip 為我們提供一種簡單的方法來觸發對 /zip 的調用 端點回到服務器上。使用函數組件模式,我們渲染一個簡單的 <h4></h4> 標記以及單擊時將觸發我們的下載的按鈕。

為了添加一些上下文,我們還引入了一個狀態值 downloading 這將允許我們有條件地禁用我們的按鈕(並更改它的文本),具體取決於我們是否已經在嘗試下載 zip。

查看handleDownloadZip() 函數,首先,我們確保通過調用 setDownloading() 暫時禁用我們的按鈕 並將其設置為 true .接下來,我們調用原生瀏覽器 fetch() 向我們的 /zip 運行 GET 請求的方法 服務器上的端點。在這裡,我們使用默認的 localhost:5001 我們的 URL 的域,因為這是服務器樣板默認運行的地方。

接下來,在 .then() fetch() 的回調 ,我們調用 response.text() 說“將原始響應正文轉換為純文本”。請記住,此時,我們希望我們的 zip 以 base64 的形式傳遞給客戶端 細繩。為了使它更有用,在下面的 .then() 回調,我們調用 b64ToBlob() b64-to-blob 中的函數 依賴。

這會將我們的 base64 字符串轉換為文件 blob(一種代表操作系統文件的瀏覽器友好格式),將 MIME 類型(編碼方法)設置為 application/zip .有了這個,我們導入並調用 fileSaver 我們之前安裝的依賴項,調用它的 .saveAs() 方法,傳入我們的 blob 以及我們希望在下載 zip 時使用的名稱。最後,我們確保 setDownloading() 返回 false 重新啟用我們的按鈕。

完畢!如果您的服務器仍在運行,請單擊該按鈕,系統會提示您下載 zip。

總結

在本教程中,我們學習瞭如何使用 JSZip 生成 zip 存檔。我們學習瞭如何使用遞歸函數將單個文件添加到 zip 以及嵌套目錄,以及如何將該 zip 文件轉換為 base64 字符串以發送回客戶端。我們還學習瞭如何在客戶端處理 base64 字符串,將其轉換為文件 blob 並使用 file-saver 將其保存到磁盤 .


Tutorial JavaScript 教程
  1. 無法在開發模式(本地主機)中通過 passport-facebook 啟用 facebook 身份驗證

  2. 如何在 ExpressJs 中使用環境變量 (env)

  3. 如何處理 Express 中的錯誤

  4. jQuery 字符串模板格式函數

  5. 部署站點時CKEditor不工作(PHP)

  6. 我從第一場比賽中學到了什麼 | R0d3nt

  7. 如何遍歷 JavaScript 中的分組對象

  1. 2019 年軟件工程師的 19 條提示

  2. 🌍FreeCodeCamp (JS) 的番茄鐘定時器 [YouTube LIVE]

  3. LeetCode 174. 地牢遊戲(javascript 解決方案)

  4. pnpm - 最好的包管理器

  5. 螺旋迭代算法

  6. 快速路由

  7. 冠狀病毒圖表網站

  1. 多存儲 DOM 事件 (Angular)

  2. 同構 ES 模塊

  3. 使用多個帖子在站點中獲取要刪除的正確 ID

  4. 可視化文檔:JavaSript array.of