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

如何在 Node.js 中流式傳輸文件以響應 HTTP 請求

如何在不阻塞服務器處理其他請求的情況下使用流發送大文件以響應 HTTP 請求。

在本教程中,我們將使用 CheatCode 的全棧 JavaScript 框架 Joystick。 Joystick 將前端 UI 框架與用於構建應用的 Node.js 後端結合在一起。

首先,我們要通過 NPM 安裝 Joystick。確保在安裝之前使用 Node.js 16+ 以確保兼容性(如果您需要學習如何安裝 Node.js 或在計算機上運行多個版本,請先閱讀本教程):

終端

npm i -g @joystick.js/cli

這將在您的計算機上全局安裝操縱桿。安裝好之後,接下來我們新建一個項目:

終端

joystick create app

幾秒鐘後,您將看到一條消息已註銷到 cd 進入你的新項目並運行 joystick start .在你這樣做之前,我們需要安裝一個依賴mime

終端

cd app && npm i mime

安裝好之後,你就可以啟動你的服務器了:

終端

joystick start

在此之後,您的應用應該可以運行了,我們可以開始了。

為什麼?

如果您正在構建一個應用程序來處理對大文件(例如,圖像、視頻或 PDF 等大文檔)的 HTTP 請求,那麼了解如何使用流非常重要。在 Node.js 中從文件系統讀取文件時,通常您可能習慣使用類似 fs.readFile() 的內容 或 fs.readFileSync() .這些方法的“陷阱”是它們將整個文件讀入內存 . 這意味著,如果您的服務器在響應請求之前使用其中任何一個來讀取文件,則會佔用運行您的應用程序的計算機的內存。

相反,流不會將任何內容加載到內存中。相反,他們將數據直接發送(或“管道”)到請求,這意味著它永遠不會加載到內存中,它只是直接傳輸。這種方法的缺點是,根據您流式傳輸到請求的文件的大小,接收端可能會有延遲(例如,當您在瀏覽器中看到視頻“緩衝區”時,它可能會接收數據作為流)。如果這對您的應用來說幾乎沒有(或沒有)問題,那麼流是最大化效率的好方法。

添加返回文件流的路由

為了展示這一點,我們將在我們剛剛在 /files/:fileName 處創建的應用程序內設置一個簡單的路由 :fileName 是一個路由參數,可以替換為任何文件的名稱(例如,video.mp4potato.png )。為了進行測試,我們將使用一些來自此人不存在的隨機生成的圖像和一段經過編輯的 VFX 圖形捲軸。本教程使用的所有文件都可以在此處從 CheatCode 的 S3 存儲桶下載。

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      // TODO: We'll implement our file stream response here...
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

首先,我們要打開 /index.server.js 當我們運行 joystick create app 時,我們剛剛創建的應用的根目錄中的文件 以上。這個文件裡面是代碼——這裡是 node.app() 函數——用於為您的應用啟動 HTTP 服務器(在後台運行 Express.js 服務器)並連接您的路由、API 和其他配置。

routes 這裡的對象,我們定義了一個屬性 /files/:fileName 分配給 Express.js 用來“處理”對該 URL 的請求的路由處理函數。就像我們之前建議的那樣,我們可以向這個路由發送一個 HTTP GET 請求,傳遞我們希望存在於 :fileName 位置的某個文件的名稱 ,例如:http://localhost:2600/files/cat.jpg .

/index.server.js

import node from "@joystick.js/node";
import fs from 'fs';
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      const filePath = `public/files/${req?.params?.fileName}`;
       
      if (fs.existsSync(filePath)) {
        // TODO: If the file exists, we'll stream it to the response here...
      }

      return res.status(404).send(`404 – File ${filePath} not found.`);
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

接下來,在該路由處理函數內部,我們創建一個變量 const filePath 它被分配給結合路徑 public/files/ 的插值字符串(意味著它需要一些純文本並在其中註入或嵌入動態值)字符串 文件名作為 :fileName 傳遞 在我們的路線中(在我們的代碼中以 req.params.fileName 的形式訪問 )。

這裡的想法是在 public 應用程序根目錄下的文件夾,我們要創建另一個文件夾 files 我們將在其中存儲文件以測試我們的流媒體。 這是任意的,純粹是舉例 .我們選擇這個位置的原因是 /public 文件夾包含我們打算的數據 公開可用和嵌套的 /files 文件夾只是一種直觀地將我們的測試數據與其他公共文件分開的方法。從技術上講,您流式傳輸的文件可以來自服務器上的任何位置。請注意不要公開您不打算公開的文件。

我們這裡最關心的是if 語句和 fs.existsSync() 傳遞給它。這個函數(從導入的fs 我們在頂部添加的依賴項——一個內置的 Node.js 庫)返回一個布爾值 truefalse 告訴我們給定的路徑是否真的存在。在我們的代碼中,我們只想在文件實際存在時對其進行流式傳輸。如果不存在,我們希望在函數的底部發回 HTTP 404 狀態碼和一條消息,讓請求者知道文件不存在。

終端

import node from "@joystick.js/node";
import fs from 'fs';
import mime from 'mime';
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/files/:fileName": (req, res) => {   
      const filePath = `public/files/${req?.params?.fileName}`;
       
      if (fs.existsSync(filePath)) {
        res.setHeader('Content-Type', mime.getType(filePath));
        res.setHeader('Content-Disposition', `attachment; filename="${req?.params?.fileName}"`);
        const stream = fs.createReadStream(filePath);
        return stream.pipe(res);
      }

      return res.status(404).send(`404 – File ${filePath} not found.`);
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

現在是重要的東西。首先,我們為 mime 添加了一個導入 包,它將幫助我們動態檢測文件的 MIME 類型(“多用途 Internet 郵件擴展”,一種受支持的用於描述多媒體文件的標準格式)。這很重要,因為我們需要與請求者進行交流什麼 流包含因此他們知道如何正確處理它。

為此,如果我們的文件存在,我們首先調用 res.setHeader() Express.js 提供的函數,傳遞我們要設置的標頭的名稱,後跟該標頭的值。這裡,Content-Type (Web 上響應類型格式的標準 HTTP 標頭)設置為 mime.getType() 的值 返回我們的 filePath .

接下來,我們設置 Content-Disposition 這是另一個標準 HTTP 標頭,其中包含有關請求者應如何處理文件的說明。這有兩個可能的值:'inline' 這表明瀏覽器/請求者應該直接加載文件,或者,attachment; filename="<name>" 這表明應該下載文件(在此處了解更多信息)。從技術上講,這種行為取決於接收文件的瀏覽器或請求者是否尊重,因此不值得強調。

接下來,本教程的重要部分:為了創建我們的流,我們調用 fs.createReadStream() 傳入 filePath 並將結果(流對象)存儲在變量 const stream 中 .現在是“魔術”部分。流的巧妙之處在於它可以在其他地方“通過管道傳輸”。 “管道”這個術語取自 Linux/Unix 系統中的相同約定,您可以在其中執行 cat settings.development.json | grep mongodb 之類的操作 (這裡是 | 管道字符告訴操作系統“移交”或“管道”cat settings.development.json的結果 到 grep mongodb )。

在我們的代碼中,我們希望將流通過管道傳輸到 Express.js res 帶有 stream.pipe(res) 的路線的 ponse 對象 (最好讀作“管道 streamres ")。換句話說,我們想用我們的文件流來響應對這個路由的請求。

而已!現在,如果我們打開瀏覽器並點擊像 http://localhost:2600/files/art.mp4 這樣的 URL (假設您使用的是從上面的 S3 存儲桶鏈接的示例文件),您應該會看到視頻開始在瀏覽器中加載。注意視頻的“加載”量如何隨著時間的推移繼續緩衝/增長。這是流向瀏覽器(我們的請求者)的流數據。

總結

在本教程中,我們學習瞭如何使用流來響應 HTTP 請求。我們學習瞭如何設置一個簡單的路由,首先檢查文件是否存在(如果不存在則返回 404),然後,如何動態檢索文件的 MIME 類型,然後創建和管道流將該文件的內容返回給我們的 HTTP 請求的響應。


Tutorial JavaScript 教程
  1. 哈剋星期五的東西 #14.08.2020

  2. 為什麼選擇哪個 JavaScript 框架很重要?

  3. 適用於所有數值數組類型的 TypeScript 函數

  4. 使用 jQuery 觸發按鍵事件的確定方法

  5. 使用 React Hook Form 構建表單

  6. 使用 VueJS 的自定義過濾器

  7. 如何使用 ExpressJS 設置 Mongoose

  1. 如何使用 Amazon S3 和 CloudFront 在 AWS 上託管 React 應用程序

  2. 通過地圖功能中的按鈕單擊刪除元素?

  3. 大 O 符號

  4. 使用 JavaScript 自動驗證在線帳戶的電子郵件

  5. React Native:經驗教訓

  6. 使用解構賦值從嵌套對像中分配變量

  7. 快速排序

  1. 更多的會議應該像 CascadiaJS

  2. 更快的靜態站點構建第 1 部分 - 僅處理您需要的內容

  3. 了解如何在 Node 中設計隊列

  4. Node.js v6.3.1 發布簡介