HTTP 事務剖析

本指南旨在讓您深入瞭解 Node.js HTTP 處理的過程。我們假設您對 HTTP 請求的工作原理有一個大致的瞭解,無論使用何種語言或程式設計環境。我們還假設您對 Node.js 的 EventEmitters(事件發射器)和 Streams(流)有一定的熟悉。如果您不太熟悉它們,建議快速閱讀一下它們的 API 文件。

建立伺服器

任何 Node Web 伺服器應用程式在某個時刻都需要建立一個 Web 伺服器物件。這可以透過使用 createServer 來完成。

const  = ('node:http');

const  = .((, ) => {
  // magic happens here!
});

傳遞給 createServer 的函式對於每個針對該伺服器的 HTTP 請求都會被呼叫一次,因此它被稱為請求處理程式。事實上,createServer 返回的 Server 物件是一個 EventEmitter,我們這裡的寫法只是建立一個 server 物件然後稍後新增監聽器的簡寫方式。

const  = http.createServer();
.on('request', (, ) => {
  // the same kind of magic happens here!
});

當一個 HTTP 請求到達伺服器時,Node 會呼叫請求處理函式,並提供幾個方便處理事務的物件:requestresponse。我們稍後會詳細介紹它們。

為了實際處理請求,需要在 server 物件上呼叫 listen 方法。在大多數情況下,您只需將希望伺服器監聽的埠號傳遞給 listen。還有一些其他選項,請查閱 API 參考

方法、URL 和請求頭

處理請求時,您可能首先要檢視的是請求的方法和 URL,以便採取適當的措施。Node.js 透過在 request 物件上放置一些方便的屬性,使這項工作相對輕鬆。

const { ,  } = request;

request 物件是 IncomingMessage 的一個例項。

這裡的 method 始終是一個標準的 HTTP 方法/動詞。url 是不包含伺服器、協議或埠的完整 URL。對於一個典型的 URL,這意味著第三個斜槓之後的所有內容。

請求頭也不難獲取。它們位於 request 物件上一個名為 headers 的獨立物件中。

const {  } = request;
const  = ['user-agent'];

這裡需要注意的重要一點是,無論客戶端實際如何傳送,所有請求頭都只以小寫形式表示。這簡化了為任何目的解析請求頭的任務。

如果某些請求頭重複出現,根據具體的請求頭,它們的值會被覆蓋或以逗號分隔的字串形式連線起來。在某些情況下,這可能會有問題,因此也提供了 rawHeaders

請求體

當接收到 POSTPUT 請求時,請求體可能對您的應用程式很重要。獲取請求體資料比訪問請求頭要複雜一些。傳遞給處理程式的 request 物件實現了 ReadableStream(可讀流)介面。這個流可以像任何其他流一樣被監聽或管道傳輸到別處。我們可以透過監聽流的 'data''end' 事件來直接從流中獲取資料。

在每個 'data' 事件中發出的資料塊是一個 Buffer。如果您知道它將是字串資料,最好的做法是將資料收集到一個數組中,然後在 'end' 事件時,將它們拼接並轉換為字串。

let  = [];
request
  .on('data',  => {
    .();
  })
  .on('end', () => {
     = .().();
    // at this point, `body` has the entire request body stored in it as a string
  });

這可能看起來有點繁瑣,而且在很多情況下確實如此。幸運的是,npm 上有像 concat-streambody 這樣的模組,可以幫助隱藏部分邏輯。在走這條路之前,對底層原理有很好的理解是很重要的,而這正是您在這裡的原因!

關於錯誤的一點提醒

由於 request 物件是一個 ReadableStream,它也是一個 EventEmitter,並且在發生錯誤時的行為也像一個事件發射器。

request 流中的錯誤透過在流上發出 'error' 事件來呈現。如果您沒有為該事件設定監聽器,錯誤將被丟擲,這可能會導致您的 Node.js 程式崩潰。因此,您應該在您的請求流上新增一個 'error' 監聽器,即使您只是記錄它然後繼續。 (不過,傳送某種 HTTP 錯誤響應可能更好。後面會詳細介紹。)

request.on('error',  => {
  // This prints the error message and stack trace to `stderr`.
  .(.stack);
});

還有其他處理這些錯誤的方法,例如使用其他抽象和工具,但請始終注意,錯誤會且確實會發生,您將不得不處理它們。

目前我們學到了什麼

到目前為止,我們已經介紹了建立伺服器,以及從請求中獲取方法、URL、請求頭和請求體。把這些都放在一起,它可能看起來像這樣:

const  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // At this point, we have the headers, method, url and body, and can now
        // do whatever we need to in order to respond to this request.
      });
  })
  .(8080); // Activates this server, listening on port 8080.

如果我們執行這個例子,我們將能夠接收請求,但無法響應它們。事實上,如果您在網頁瀏覽器中訪問這個例子,您的請求將會超時,因為沒有任何東西被髮送回客戶端。

到目前為止,我們還沒有接觸到 response 物件,它是 ServerResponse 的一個例項,而它又是一個 WritableStream(可寫流)。它包含了許多有用的方法,用於向客戶端傳送資料。我們接下來會介紹這個。

HTTP 狀態碼

如果您不設定它,響應的 HTTP 狀態碼將始終是 200。當然,並非每個 HTTP 響應都應該如此,並且在某些時候您肯定會想傳送一個不同的狀態碼。為此,您可以設定 statusCode 屬性。

. = 404; // Tell the client that the resource wasn't found.

還有一些其他的快捷方式,我們很快就會看到。

設定響應頭

響應頭可以透過一個方便的方法 setHeader 來設定。

response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');

在設定響應頭時,它們的名稱是大小寫不敏感的。如果您重複設定一個頭,您設定的最後一個值就是最終傳送的值。

顯式傳送響應頭資料

我們已經討論過的設定響應頭和狀態碼的方法都假定您正在使用“隱式響應頭”。這意味著您依賴 Node 在您開始傳送響應體資料之前,在正確的時間為您傳送響應頭。

如果您願意,您可以顯式地將響應頭寫入響應流。為此,有一個名為 writeHead 的方法,它將狀態碼和響應頭寫入流中。

response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
});

一旦您設定了響應頭(無論是隱式還是顯式),您就可以開始傳送響應資料了。

傳送響應體

由於 response 物件是一個 WritableStream,向客戶端寫出響應體只是使用通常的流方法的問題。

response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

流上的 end 函式也可以接受一些可選的資料作為流的最後一部分資料傳送,所以我們可以將上面的例子簡化如下。

response.end('<html><body><h1>Hello, World!</h1></body></html>');

重要的是要在開始向響應體寫入資料塊之前設定狀態和響應頭。這是合理的,因為在 HTTP 響應中,響應頭位於響應體之前。

關於錯誤的另一件小事

response 流也可能發出 'error' 事件,在某個時候您也需要處理這個問題。所有關於 request 流錯誤的建議在這裡仍然適用。

整合起來

現在我們已經學習瞭如何建立 HTTP 響應,讓我們把所有內容整合起來。在前面的例子基礎上,我們將建立一個伺服器,它會將使用者傳送給我們的所有資料都發回。我們將使用 JSON.stringify 將這些資料格式化為 JSON。

const  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // BEGINNING OF NEW STUFF

        .('error',  => {
          .();
        });

        . = 200;
        .('Content-Type', 'application/json');
        // Note: the 2 lines above could be replaced with this next one:
        // response.writeHead(200, {'Content-Type': 'application/json'})

        const  = { , , ,  };

        .(.());
        .();
        // Note: the 2 lines above could be replaced with this next one:
        // response.end(JSON.stringify(responseBody))

        // END OF NEW STUFF
      });
  })
  .(8080);

回顯伺服器示例

讓我們簡化前面的例子,來製作一個簡單的回顯伺服器,它只是將請求中收到的任何資料直接在響應中發回。我們所需要做的就是從請求流中獲取資料,並將這些資料寫入響應流,這與我們之前做的類似。

const  = ('node:http');


  .((, ) => {
    let  = [];
    
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        .();
      });
  })
  .(8080);

現在讓我們調整一下。我們希望只在以下條件下發送回顯:

  • 請求方法是 POST。
  • URL 是 /echo

在任何其他情況下,我們只想響應一個 404。

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      let  = [];
      
        .('data',  => {
          .();
        })
        .('end', () => {
           = .().();
          .();
        });
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

透過這種方式檢查 URL,我們實際上在做一種“路由”。其他形式的路由可以像 switch 語句一樣簡單,也可以像 express 這樣的完整框架一樣複雜。如果您正在尋找一個只做路由而不做其他事情的工具,可以試試 router

太棒了!現在讓我們嘗試簡化這個。記住,request 物件是一個 ReadableStream,而 response 物件是一個 WritableStream。這意味著我們可以使用 pipe 將資料從一個流導向另一個流。這正是我們回顯伺服器所需要的!

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

流真棒!

不過我們還沒有完全完成。正如本指南中多次提到的,錯誤可能並且確實會發生,我們需要處理它們。

為了處理請求流上的錯誤,我們將錯誤記錄到 stderr 併發送一個 400 狀態碼來表示一個 Bad Request(錯誤請求)。然而,在一個真實世界的應用程式中,我們會希望檢查錯誤以確定正確的狀態碼和訊息應該是什麼。和往常一樣,處理錯誤時,您應該查閱 Error 文件

對於響應,我們只將錯誤記錄到 stderr

const  = ('node:http');


  .((, ) => {
    .('error',  => {
      .();
      . = 400;
      .();
    });
    .('error',  => {
      .();
    });
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

我們現在已經涵蓋了處理 HTTP 請求的大部分基礎知識。此時,您應該能夠:

  • 使用請求處理函式例項化一個 HTTP 伺服器,並讓它監聽一個埠。
  • request 物件中獲取請求頭、URL、方法和請求體資料。
  • 基於 URL 和/或 request 物件中的其他資料做出路由決策。
  • 透過 response 物件傳送響應頭、HTTP 狀態碼和響應體資料。
  • 將資料從 request 物件管道傳輸到 response 物件。
  • 處理 requestresponse 流中的流錯誤。

基於這些基礎知識,可以構建適用於許多典型用例的 Node.js HTTP 伺服器。這些 API 還提供了許多其他功能,所以請務必通讀 EventEmittersStreamsHTTP 的 API 文件。