[Node.js] 請求與響應

首先介紹在上一 Node.js 系列文章中出現的 reqres 物件。

Request Object

將上一筆記中的 server.js 第四行 console.log('request made'); 改為 console.log(req);

1
2
3
4
5
6
7
8
9
const http = require('http');

const server = http.createServer(( req, res) => { // 兩個參數 req 為請求物件,res 為響應回覆物件
console.log(req); // 修改該行,列印出 req 物件
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

執行並在瀏覽器連上 localhost:3000

可以看到 req 物件包含了很多東西,例如標頭,標頭是有關請求類型的 meta data 元數據預期的響應類型…等等。


我們可以再改成如下

1
2
3
4
5
6
7
8
9
10
const http = require('http');

const server = http.createServer(( req, res) => { // 兩個參數 req 為請求物件,res 為響應回覆物件
console.log(req.url);
console.log(req.method);
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

執行結果

req.url/ ,不是 localhost:3000 是因為它以此為根,所以才顯示 /,試著將網址後面添加 /about

可以看到結果如下

req.method 表示是一個使用 GET 方法的請求

Response Object

在前一章的筆記中,連上了 localhost:3000 卻顯示找不到網頁的訊息是因為我們沒有做響應的處理,接下來就要透過 response 物件處理這部分。

現在我們想提出某種類型的內容回應,要制定相對應的 response header 響應頭給予瀏覽器對於將接收的響應更多信息。

1
2
3
4
5
6
7
8
9
10
11
12
const http = require('http');

const server = http.createServer(( req, res) => {
console.log(req.url);
console.log(req.method);

res.setHeader('Content-Type', 'text/plain'); // set header content type
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

Content-Type 表示內容類型,可以是 text, HTML, 或是 json,例如 'text/plain' 即發送純文本給瀏覽器。

設置完 response header,接著實際發送數據到瀏覽器,使用 res.write(),還要加上 res.end() 表示響應結束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require('http');

const server = http.createServer(( req, res) => {
console.log(req.url);
console.log(req.method);

res.setHeader('Content-Type', 'text/plain'); // set header content type
res.write('Hello, Joe!'); // write a response to the client
res.end(); // end the response
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

接著執行並且連上 localhost:3000,出現以下內容

我們可以開啟「開發人員工具」的 Network 標籤 ( F12 或是 Ctrl + shift + I 開啟)

按 Ctrl + R 刷新

點擊 localhost

選擇 Headers 標籤可以看到 Response Headers 當中的內容類型為 text/plain,與我們在伺服器設定的一樣


若是我們想回應一些 HTML 內容,而不是純文本呢?

首先更改響應標頭,將內容類型改為 'text/html'

1
res.setHeader('Content-Type', 'text/html');

接著修改內容

1
2
res.write('<p>Hello, Joe!</p>');
res.write('<p>Hello again, Joe!</p>');

目前的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http');

const server = http.createServer(( req, res) => {
console.log(req.url);
console.log(req.method);

res.setHeader('Content-Type', 'text/html');
res.write('<p>Hello, Joe!</p>');
res.write('<p>Hello again, Joe!</p>');
res.end();
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

執行

可以檢查這些內容發現為 p 元素
當然也可以將內容添加 head 等其它元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require('http');

const server = http.createServer(( req, res) => {
console.log(req.url);
console.log(req.method);

res.setHeader('Content-Type', 'text/html');
res.write('<head><link rel="stylesheet" href="#"></head>');
res.write('<p>Hello, Joe!</p>');
res.write('<p>Hello again, Joe!</p>');
res.end();
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

雖然這樣可以回應 HTML 給瀏覽器,但如果我們像這樣添加更多的內容會非常的混亂,所以這不是一個好方法。

我們應該創建 HTML 在一個單獨的文件中,然後使用 Node.js 讀取這些文件並發送給瀏覽器,所以要再次使用上個章節介紹的 fs 檔案系統模組。

回傳 HTML 頁面

首先建立一個資料夾名為 views ,用於存放要回應的 HTML 檔案

index.html 內容

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node.js Test</title>
</head>
<body>
<h1>Home</h1>
<p>This is a test!</p>
</body>
</html>

接著修改 server.js,新增

1
const fs = require('fs');

內容類型不變,使用 fs.readFile() 讀取檔案並在沒有 err 的情況下響應到瀏覽器

1
2
3
4
5
6
7
8
9
10
res.setHeader('Content-Type', 'text/html');
fs.readFile('./views/index.html', (err, data) => { // send a HTML file
if (err) {
console.log(err);
res.end(); // 即使錯誤也要結束響應
} else {
res.write(data);
res.end();
}
})

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http');
const fs = require('fs');

const server = http.createServer(( req, res) => {
res.setHeader('Content-Type', 'text/html');
fs.readFile('./views/index.html', (err, data) => { // send a HTML file
if (err) {
console.log(err);
res.end(); // 即使錯誤也要結束響應
} else {
res.write(data);
res.end();
}
});
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000')
});

接著執行,重新整理 localhost:3000 頁面,結果顯示了 index.html

另外如果只寫入一樣東西到響應,可以寫在 res.end() 即可,它仍會執行相同的操作

1
2
// res.write(data);
res.end(data);

若是要編寫多個內容,則寫多個 res.write() ,最後 res.end()

以上就是一個將 HTML 頁面發送到瀏覽器的方式。

基礎路由

現在不論你在 localhost:3000/ 後面加什麼內容

所引導的響應都一樣會是 index.html

但我們想根據訪問的路線來響應不一樣的頁面,所以我們需要一種方法找到發出請求的 URL,並根據此 URL 響應相對應的頁面。
(前面有使用 req.url 顯示)

首先在 views 資料夾新增 about.html 及 404.html


404.html 內容,用於使用者訪問不存在的路線時響應的頁面

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node.js Test</title>
</head>
<body>
<h1>404 哎呀!找不到該頁面!</h1>
<p>該頁面不存在喔!</p>
</body>
</html>

about.html 內容,用於路徑為 /about 時,響應該頁面

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Node.js Test</title>
</head>
<body>
<h1>About</h1>
<p>This is an about page.</p>
</body>
</html>

接著修改 server.js ,path 為要響應的檔案路徑,使用 switch 來判斷使用者輸入了何種路徑

1
2
3
4
5
6
7
8
9
10
11
12
let path = "./views/"; // 要從 views 取其中的檔案來響應
switch(req.url) { // 根據 req.url 判斷路徑
case '/':
path += 'index.html';
break;
case '/about':
path += 'about.html';
break;
default: // 前面的選項都不符合,最後就是找不到該頁面了
path += '404.html';
break;
}

讀取檔案路徑改為讀取 path

1
fs.readFile(path, (err, data) => {

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const http = require('http');
const fs = require('fs');

const server = http.createServer(( req, res) => {
let path = "./views/"; // 要從 views 取其中的檔案來響應
switch(req.url) { // 根據 req.url 判斷路徑
case '/':
path += 'index.html';
break;
case '/about':
path += 'about.html';
break;
default: // 前面的選項都不符合,最後就是找不到該頁面了
path += '404.html';
break;
}

res.setHeader('Content-Type', 'text/html'); // set header content type
fs.readFile(path, (err, data) => { // send a HTML file
if (err) {
console.log(err);
res.end(); // 即使錯誤也要結束響應
} else {
// res.write(data);
res.end(data);
}
});
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000');
});

現在執行並輸入各種路徑觀察
顯示首頁



顯示 about 頁面



隨便輸入路徑,顯示 404 頁面



以上可以讓我們自由的根據 URL 響應多個不同的頁面。

Status Codes 狀態代碼

在連到找不到網頁的情況下

開啟開發人員工具的 Network,注意到 123 的 status 為 200,此為現在要介紹的 status codes 狀態代碼


響應的狀態代碼,描述了響應發送到瀏覽器的狀態,常見有以下:

  1. 200 - OK 一切正常
  2. 301 - Resource moved 資源已永久移動到某處,表示請求資源的 URL 已被改變
  3. 404 - Not found 找不到該頁面
  4. 500 - Internal server error 內部的伺服器錯誤

有更多的狀態代碼,通常分布於 100, 200, 300, 400, 500 的範圍

  1. 資訊回應 (Informational responses, 100–199),
  2. 成功回應,按計畫進行 (Successful responses, 200–299),
  3. 重定向 (Redirects, 300–399),
  4. 用戶或客戶端的錯誤 (Client errors, 400–499),
  5. 伺服器端的錯誤 (Server errors, 500–599).

現在添加這些狀態代碼到我們的響應中,在 switch 中對 res.statusCode 進行設置, '/''/about' 都是正常的狀態,除此之外則為 404 找不到網頁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch(req.url) {
case '/':
path += 'index.html';
res.statusCode = 200; // 一切正常
break;
case '/about':
path += 'about.html';
res.statusCode = 200;
break;
default:
path += '404.html';
res.statusCode = 404; // 該資源不存在
break;
}

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const http = require('http');
const fs = require('fs');

const server = http.createServer(( req, res) => {
let path = "./views/";
switch(req.url) {
case '/':
path += 'index.html';
res.statusCode = 200; // 一切正常
break;
case '/about':
path += 'about.html';
res.statusCode = 200;
break;
default:
path += '404.html';
res.statusCode = 404; // 該資源不存在
break;
}

res.setHeader('Content-Type', 'text/html');
fs.readFile(path, (err, data) => {
if (err) {
console.log(err);
res.end();
} else {
// res.write(data);
res.end(data);
}
});
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000');
});

執行,依舊連到 /123,發現狀態為 404 了(favicon.ico 與我們無關,這裡不管)

可以連上 /about

或是 / ,狀態都為 200


Redirects 重新定向

假設我們有一個網站,其中一個路徑為 '/about-me',但後來決定將該路徑改為 '/about',依照前面的做法將 switch 中的 URL 從 '/about-me' 改成 '/about' 即可,但萬一我的網站很受歡迎,有成千上萬的人點擊過去的連結,則會連到 404 的頁面(因為該頁面已不在),所以我們應該偵測 '/about-me' 的 URL,並將其重新導向 '/about' 的頁面。

switch 新增 '/about-me' 的 case,雖然也可以用 path 指向 about.html 的方式,但這裡要做的是「重新定向」,首先設定 statusCode 為 301,表示進行了永久的重新定向,接著設定標頭,第一個參數是 Location 屬性,第二個參數則是重新定向到哪裡,這裡即 '/about',最後要加上該次的響應結束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch(req.url) {
case '/':
path += 'index.html';
res.statusCode = 200;
break;
case '/about':
path += 'about.html';
res.statusCode = 200;
break;
case '/about-me':
// path += 'about.html'; 不使用該方法
res.statusCode = 301; // 表示進行了永久的重新定向
res.setHeader('Location', '/about'); // 重新定向到 /about 路徑
res.end();
break;
default:
path += '404.html';
res.statusCode = 404;
break;
}

全部的 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const http = require('http');
const fs = require('fs');

const server = http.createServer(( req, res) => {
let path = "./views/";
switch(req.url) {
case '/':
path += 'index.html';
res.statusCode = 200;
break;
case '/about':
path += 'about.html';
res.statusCode = 200;
break;
case '/about-me':
// path += 'about.html'; 不使用該方法
res.statusCode = 301; // 表示進行了永久的重新定向
res.setHeader('Location', '/about'); // 重新定向到 /about 路徑
res.end();
break;
default:
path += '404.html';
res.statusCode = 404;
break;
}

res.setHeader('Content-Type', 'text/html');
fs.readFile(path, (err, data) => {
if (err) {
console.log(err);
res.end();
} else {
// res.write(data);
res.end(data);
}
});
});

server.listen(3000, 'localhost', () => {
console.log('listening for requests on port 3000');
});

現在執行,輸入以下網址

頁面會自動引導到 /about 頁面

開啟開發人員工具 Network 標籤,觀察到 about-me 狀態為 301

觀看其 Headers 可以看到 Location 的部分


以上為伺服器響應網頁的基礎,但隨著網站越來越大,變得更複雜,要處理許多不同類型的請求,如發布、刪除請求以及有關數據庫的邏輯… 等等,這些以上面所學的方式處理會有些混亂、難維護,但幸運的是有名為 Express 的第三方框架,可以幫助我們更輕鬆的處理、管理以上問題。

即使如此我們也該先了解 Node.js 在這中間的過程,再學習 Express 會對於為什麼這麼做比較有感受及輕鬆。



參考資料
The Net Ninja | Node.js Crash Course Tutorial #4 - Requests & Responses