- 原文地址:Node.js Fundamentals: Web Server Without Dependencies
- 原文做者:Seva Zaikov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Mirosalva
- 校對者:kasheemlew,Endone
Node.js 是構建 web 應用服務端的一種很是流行的技術選擇,而且有許多成熟的網絡框架,好比 express, koa, hapijs。儘管如此,在這篇教程中咱們不用任何依賴,僅僅使用 Node 核心的 http 包搭建服務端,並一點點地探索全部的重要細節。這不是你能常常看到的一種情況,它能夠幫助你更好地理解上面說起的全部框架--現有的許多庫不只在底層使用這個包,並且常常會將原始對象暴露出來,使得你能夠在某些特殊任務中應用他們。html
首先,讓咱們開始一個最簡單的程序--返回那句經典的響應『hello,world』。爲了用 Node.js 構建一個服務程序,咱們須要使用 http 內建模模塊,尤爲是 createServer 函數。前端
const { createServer } = require("http");
// 這是一種好的實現
// 容許運行在不一樣的端口
const PORT = process.env.PORT || 8080;
const server = createServer();
server.on("request", (request, response) => {
response.end("Hello, world!");
});
server.listen(PORT, () => {
console.log(`starting server at port ${PORT}`);
});
複製代碼
讓咱們列出這個簡短示例的全部內容:node
request
事件添加一個事件監聽器咱們建立的服務程序是 http.Server 類的一個實例,繼承自對象 net.Server,而它又繼承自類 EventEmitter。有許多咱們能夠監聽的事件,但最重要的事件是 request,而且在建立服務時提供它的監聽,常見的實現方式以下:android
const { createServer } = require("http");
// 這樣等同於 `server.on('request', fn);`
createServer((request, response) => {
response.end("Hello, world!");
}).listen(8080);
複製代碼
最後一步是啓動咱們的服務。我經過調用 server.listen 方法來啓動,而且你能夠指定端口和啓動後執行內容。有一點要注意的是:服務並不會當即開始,它接入來訪的請求時必須先和一個端口綁定,然而在實踐中這點並非很是重要,由於這個過程幾乎是瞬間完成。你也能夠經過 listening 事件方法來單獨監聽這個特殊事件。ios
如今,在咱們學會了如何實例化一個新服務應用後,讓咱們看看如何實際回覆用戶的請求。在咱們惟一的事件處理器中,咱們使用 response.end 方法以常規經典響應 Hello, world! 來回復。你能夠看出這個簽名與可寫流方法 writable.end 很是類似,這是由於請求和響應對象都是流對象 streams,同時請求只是可讀流,並且響應只是可寫流。爲何它們必須是流對象呢?爲何咱們不能發送整個回覆?git
答案是在回覆前咱們不是非得作完全部的事。想象這種情景,當咱們從文件系統中讀取一個文件時,而這個文件比較大。所以咱們能夠經過 fs.createReadStream 方法打開了一個文件流,這樣咱們就能夠當即寫入響應。此外咱們還能夠直接將輸入經過管道鏈接到輸出!github
如今由於它是流對象,咱們能夠作下面的事:web
const { createServer } = require("http");
createServer((request, response) => {
response.write("Hello");
response.write(", ");
response.write("World!");
response.end();
}).listen(8080);
複製代碼
所以咱們能夠直接屢次寫入咱們流對象。在任何形式的循環中這麼作時要當心,由於你必須本身處理背壓問題,另外最好直接管道鏈接到流對象。一樣的,請注意在結尾時使用 response.end()
方法。這是強制的,若是沒有這個調用,Node 將保持此鏈接處於打開狀態,形成內存泄漏和客戶端處於等待狀態。數據庫
最後,讓咱們演示一下流的管道方法是如何爲響應對象和其餘流起做用的。爲了這麼作,咱們使用 __filename 變量來讀取源文件:express
const { createReadStream } = require("fs");
const { createServer } = require("http");
createServer((request, response) => {
createReadStream(__filename).pipe(response);
}).listen(8080);
複製代碼
咱們不必定要手動調用 res.end
方法,由於在原始流結束時,它也會自動地關閉管道傳輸的流。
咱們的服務程序實現了 HTTP 協議,它是一種文本集的規則,容許客戶端以本身首選格式請求特定信息,也容許服務程序以數據和附加信息來回復,例如格式、鏈接狀態、緩存信息等等。
讓咱們看一個對 web 頁面的典型請求:
GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko)
Host: blog.bloomca.me
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive
複製代碼
這是當你請求頁面時,咱們瀏覽器發送的內容,除了上面這些它還發送更多的 headers,傳輸 cookies(也是一種 header),還有其餘信息。對咱們來講重要的是要理解:全部的請求有方法、路徑(路由)以及 headers 列表,這些都是鍵值對(若是你想了解 cookies,它們只是一種具備特殊含義的 header)。HTTP 是一種文本協議,正如你所看到的,你本身能夠讀懂它。雖然它只是一組協議,實現此協議的瀏覽器和服務程序都試圖遵照這個協議規定,這就是整個互聯網的運轉方式。並不是全部規則都被遵照,但主要規則 - HTTP 操做、路由、cookie 都足夠可靠,您應該始終追求可預測的行爲。
我能夠經過 request.headers 屬性來訪問客戶端發送的全部 header。例如爲了識別客戶端選擇的語言類型,咱們能夠像下面這樣作:
const { createServer } = require("http");
createServer((request, response) => {
// 這個對象中全部的 header 都是小寫
const languages = request.headers["accept-language"];
response.end(languages);
}).listen(8080);
複製代碼
我我的對語言的選擇,使用『en-US,en;q=0.9,ru;q=0.8,de;q=0.7』,也就是說我首選英語,其次俄語,最後是德語。通常狀況下瀏覽器使用你的操做系統語言,可是它會被替換,不是最好的依賴,由於用戶不能直接控制它(而且不一樣瀏覽器對這行代碼有不一樣的選擇)。
爲了寫一個 header,你須要理解 HTTP 是一種協議,這個協議規定首先是元數據,而後在一個分隔符(兩個換行符)以後纔是真正的報文體。這意味着一旦你開始發送內容,你就不能變動你的報文頭!若是這麼作會在 Node 中拋出錯誤以及實際會停止你的程序。
有兩種設置 header 的方法: response.setHeader 方法和 response.writeHead 方法。 二者的區別是前者更特殊,而且若是二者都被使用的狀況下,全部的 header 會被合併,且以 writeHead 方式設置的 header 取值具備更高的優先級。writeHead 和 write 方法的做用相同,也就是說你不能夠在後續修改 header。
const { createServer } = require("http");
createServer((req, res) => {
res.setHeader("content-type", "application/json");
// 咱們須要發送 Buffer 或者 String 類型數據,咱們不能直接傳遞一個對象
res.end(JSON.stringify({ a: 2 }));
}).listen(8080);
複製代碼
HTTP 定義了每一個響應都必需要有的狀態碼,列表 中定義了各個狀態碼的含義。一樣,並不是全部人都嚴格遵照這個列表
讓咱們列出最重要的狀態碼:
2xx – 成功碼:
3xx – 重定向碼
注意 301/302 狀態碼。瀏覽器傾向於記住 301,若是你偶然地把一些 URL 標記上 301 狀態碼,瀏覽器在收到新響應後也許仍然會這麼作(它們甚至都不檢查)。
4xx - 客戶端錯誤碼
5xx – 服務器錯誤碼
這些錯誤碼是最多見的類型,而且足夠讓你爲請求匹配正確的狀態碼。在 Node.js 中,咱們既可使用 response.statusCode 方法,也可使用 response.writeHead 方法。此次就讓咱們使用 writeHead 方法來設置一個自定義 HTTP 消息:
const { createServer } = require("http");
createServer((req, res) => {
// 代表沒有內容
res.writeHead(204, "My Custom Message");
res.end();
}).listen(8080);
複製代碼
若是你嘗試在瀏覽器中打開這些代碼,而且在『網絡』標籤中瀏覽 HTML 請求,你將會看到『狀態碼:204 個人自定義消息』。
在 Node.js 服務程序中,全部的請求都由單個請求處理程序處理。咱們能夠經過運行咱們的任何服務來測試這點,或者經過請求不一樣的 URL 地址,例如地址 http://localhost:8080/home 和 http://localhost:8080/about。你能夠看到測試將返回一樣的響應。然而,在請求對象中咱們有一個屬性 request.url,咱們可使用它構建一個簡單的路由功能:
const { createServer } = require("http");
createServer((req, res) => {
switch (req.url) {
case "/":
res.end("You are on the main page!");
break;
case "/about":
res.end("You are on about page!");
break;
default:
res.statusCode = 404;
res.end("Page not found!");
}
}).listen(8080);
複製代碼
有不少警告(嘗試在 /about/
頁面添加一個尾部斜槓),可是你有辦法。在全部的框架中,有一個主處理程序,它將全部請求導向已註冊的處理程序。
你可能熟悉 HTTP methods/verbs,例如 GET 和 POST。它們是 HTTP 協議自己的一部分,且含義很明顯。然而,它們也有許多我不想深挖的微妙細節,爲了簡潔起見,我想說 GET 是爲了獲取數據,而 POST 是爲了建立新的實體對象。沒人不讓你拿它們另作他用,可是標準和慣例建議你不要這麼作。
上面已經說到,在 Node.js 中服務程序有 request.method 屬性,能夠用於咱們內部邏輯處理。一樣,Node.js 自己沒有任何內容可供咱們使用,對不一樣方法抽象出處理方法。咱們須要本身構建抽象處理方法:
const { createServer } = require("http");
createServer((req, res) => {
if (req.method === "GET") {
return res.end("List of data");
} else if (req.method === "POST") {
// 建立新實體
return res.end("success");
} else {
res.statusCode(400);
return res.end("Unsupported method");
}
}).listen(8080);
複製代碼
Cookies 值得單獨開一個文章來介紹,因此請隨時閱讀更多關於它們的內容 MDN guide。
兩個關鍵詞,cookie 用於在請求過程當中保留一些數據,由於 HTTP 是一種無狀態協議,從技術上講,若是沒有 cookies(或者本地存儲),咱們必須在每次須要身份驗證的操做以前都得執行登陸操做。咱們在客戶端保留 cookie(一般在瀏覽器中),這樣瀏覽器能夠給咱們發送一個名爲 Cookie 且包含全部 cookie 對象的 header,咱們能夠經過一個 Set-Cookie
header 來響應請求,告訴客戶端設置哪一個 cookie(例如訪問 token);客戶端保存它以後,就會在每次後續請求中將它發回服務端。
讓咱們運行下面的代碼:
const { createServer } = require("http");
createServer((req, res) => {
res.setHeader(
"Set-Cookie",
["myCookie=myValue"],
["mySecondCookie=mySecondValue"]
);
res.end(`Your cookies are: ${req.headers.cookie}`);
}).listen(8080);
複製代碼
你第一次刷新瀏覽器時,可能會看到一些舊緩存 cookie,可是你看不到 myCookie 或者 mySecondCookie。然而,若是你再刷新瀏覽器,你將會看到二者的值!這個狀況的緣由是在響應後客戶端會在 cookies 中設置它們的值,正是這個響應渲染了咱們頁面。所以咱們只會在下一次請求發生後纔會從客戶端接收到這些返回的緩存 cookies。
如今,若是咱們想在代碼中使用 cookie 值該怎麼辦呢?Cookie 在 HTTP 中只是一個 header,所以它是一個有着本身規則的字符串--cookie 使用 key=value
的模式來編寫,包含參數,以 ;
符號分割。你能夠編寫本身的解析器(相似這篇文章這樣this SO answer),可是我建議你使用與你的框架或庫兼容的其餘外部庫做選擇就好了。
一樣地,請注意你不能刪除 cookie,由於它屬於客戶端,可是你能夠經過設置它爲一個空值或一個過去的失效日期這種方式,使它變得無效。
給特殊處理器設置參數很常見:例如,你但願顯示全部圖片,咱們能夠指定一個頁面,這經過能夠經過查詢參數來實現。它們被添加到 URL,經過符號 ?
與路徑分隔開:http://localhost:8080/pictures?page=2
,你能夠看出,咱們請求了圖片庫的第二個頁面。或者咱們能夠只須要把它嵌入到 URL 連接自己,可是這裏的問題是:若是有不止一個參數,URL 會很快變得混亂。查詢參數並不固定,所以咱們能夠添加任意數量的內容,也能夠在未來刪除/添加新內容。
爲了在咱們的服務程序中獲取到它,咱們使用 request.url 屬性,在 路由 小節中咱們已經用到過。如今,咱們須要將咱們的 URL 與查詢參數分開,雖然咱們能夠手動這麼作,可是沒有必要,由於它已經在 Node.js 中實現了:
const { createServer } = require("http");
createServer((req, res) => {
const { query } = require("url").parse(req.url, true);
if (query.name) {
res.end(`You requested parameter name with value ${query.name}`);
} else {
res.end("Hello!");
}
}).listen(8080);
複製代碼
如今,若是你添加查詢參數來請求任何頁面,你將會在響應中看到效果,例如這個 http://localhost:8080/about?name=Seva 的請求將會返回帶有咱們標識名的字符串:
你的請求參數名帶有值 Seva
複製代碼
咱們最後要看的是請求體內容。以前咱們已知道,你能夠從 URL 自己獲取全部信息(路由和查詢參數),可是咱們如何從客戶端獲取到真實數據?你不用直接訪問它,但咱們能夠直接經過讀取流來得到傳遞的數據,這也是爲何請求對象是流對象的一個緣由。讓咱們寫一個簡單的服務程序,這個程序指望從 POST 請求中獲取一個 JSON 對象,而且當獲取的並不是有效 JSON 時將返回 400 狀態碼。
const { createServer } = require("http");
createServer((req, res) => {
if (req.method === "POST") {
let data = "";
req.on("data", chunk => {
data += chunk;
});
req.on("end", () => {
try {
const requestData = JSON.parse(data);
requestData.ourMessage = "success";
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(requestData));
} catch (e) {
res.statusCode = 400;
res.end("Invalid JSON");
}
});
} else {
res.statusCode = 400;
res.end("Unsupported method, please POST a JSON object");
}
}).listen(8080);
複製代碼
最簡單的測試它的方法是使用 curl。首先,使用一個 GET 方法來查詢:
> curl http://localhost:8080
Unsupported method, please POST a JSON object
複製代碼
如今,使用一個隨機字符串做爲咱們的數據來發起一個 POST 請求
> curl -X POST -d "some random string" http://localhost:8080
Invalid JSON
複製代碼
最後,產生一個正確的響應並查看結果:
> curl -X POST -d '{"property": true}' http://localhost:8080
{"property":true,"ourMessage":"success"}
複製代碼
你能夠看出,有在僅使用內建模塊來處理每一個請求時有許多繁瑣工做 - 好比記住每次都要關閉響應流,或者每次你發送對象時都要以字符串化的 JSON 來設置一個 Content-Type: application/json
類型的 header,或者分析查詢參數,或者編寫你本身的路由系統.....全部這些都被完成,只須要記住在框架引擎下,它使用這些核心方法,你不用擔憂它的內部實際如何運行。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。