是 Ray 不是 Array

整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ

Advertisement
2023-09-21 Nodejs

Day7 - Web Framework to Express.js

Web Framework to Express
Web Framework to Express

前言

前一篇我們嘗試用 Node.js 建立了 HTTP API,我相信你應該學到相當的多,但是實戰上來講,我們並不會直接使用 Node.js 來建立 HTTP API,因為這樣會太麻煩了,因此我們會使用一些框架來幫助我們建立 HTTP API,而這邊我們就來介紹一個非常熱門的框架,也就是 Express.js。

什麼是 Express.js?

什麼是 Express.js(又稱 Express) 呢?如果你本身具有前端開發經驗的話,你可以把它想像成是 Vue & Angular 為什麼呢?因為這兩個定位與 Express.js 非常相似,它們都是一個 Web 框架,只是 Vue & Angular 是用來開發網頁前端的,而 Express.js 則是專門針對 Node.js 的伺服器開發的框架。

Note
由於 React 在官方定義上是 Library,因此才沒有提到 React,但其實 Library 跟 Framework 的界線越來越模糊就是了。

Express 目前是廣泛被利用的框架,舉例來講…Paypal 與 Uber 就有使用到 Express.js 來開發伺服器,由此可知,我們可以知道 Express 是一個非常成熟的框架,因此我們可以放心的使用它來開發伺服器。

當然,除了 Express 還有其他替代品可以選擇,如:Koa.js、Fastify、NestJS 等等,但這一篇我們會比較著重於使用 Express 與介紹 Express 的相關知識,至於其他框架的部分…我們有緣再來介紹 :D

有緣再來介紹
有緣再來介紹

建立專案

起手式請你依照以下指令來建立專案

1
mkdir express-example

接著請你進入專案中

1
cd express-example

請別忘了初始化專案

1
git init
1
npm init -y

接著請你安裝 Express

1
npm install express

Note
這邊我們使用 npm install express 來安裝 Express,但是你也可以使用 npm install express --save 來安裝,這兩個指令結果是相同的,因為 --save 是預設的,因此你可以省略;甚至你可以簡寫成 npm i express,因為 iinstall 的縮寫。

撰寫 Express

接下來我們要來建立一個我們的第一支檔案,也就是 index.js

1
touch index.js

建立好後你可能會想說我們第一行應該是 const http = require('node:http'); 對吧?但是這邊我們不會這樣做,因為我們要使用 Express,因此我們會使用 require('express') 來引入 Express

1
const express = require('express');

接著我們會需要將這個 Express 套件呼叫出來,因此我們會將 express() 呼叫出來,並且將它指派給 app 這個變數

1
2
const express = require('express');
const app = express();

接著我們會需要一個伺服器,因此我們會使用 app.listen() 來建立伺服器,並且指定 3000 這個 port,這邊我先順便補個簡單的 HTTP API,讓你可以在瀏覽器上看到結果

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.send('Hello, World!');
});

app.listen(3000)

接下來你可以嘗試運行專案了,不意外你應該是可以在瀏覽器上看到「Hello, World!」。

「疑?就這麼簡單?」

沒錯,就是這個簡單,恭喜你體驗到 Web Framework 的威力了!我們這就是人家常在講的…

「站在巨人的肩膀上,你可以看得更遠。」

那麼我們可以相比一下我們前面寫的程式碼與使用 Express 後的程式碼

純 Node.js:

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

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
if (req.method === 'GET') {
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Hello, World!' }));
}
});

server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

使用 Express:

1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/', (req, res) => {
res.send('Hello, World!');
});

app.listen(3000)

透過兩者之間的比較,你應該也可以深刻的體會到為什麼我們要使用 Express 了,畢竟它幫我們省去了很多的麻煩,讓我們可以專注在開發上,而不是在處理一些瑣碎的事情。

那麼這邊我也直接示範一下把前面章節我們所寫的 TodoList API 改成 Express 版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express');
const app = express();
const port = 3000;

const data = [];

app.get('/todos', (req, res) => {
res.send(data);
});

app.post('/todos', (req, res) => {
const { title, completed } = req.body;

data.push({
id: new Date().getTime(),
title,
completed,
});

res.send(data);
});

app.listen(3000)

但是當你改成這樣後,去嘗試戳你會發現 Post /todos 會無法正常運作,這是因為我們需要一個東西來幫助我們解析 req.body,那該怎麼做呢?很簡單,補上 app.use(express.json()); 就可以了

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

const app = express();
const port = 3000;

const data = [];

app.use(express.json());

// ... 略過其他程式碼

app.listen(3000)

這樣子你在戳 Post /todos 就可以正常運作了。

Middleware

什麼是 Middleware 呢?你可以把它想像成中間人,以比較生活面的例子來講的話,你可以把它想像成你想要買房 or 租房,所以你會先經過仲介,然後仲介會幫你找到適合的房子,而這樣的過程就是中間層,而在程式碼上來講,你可以把它想像成一個函式,這個函式會在你的程式碼執行前,先執行這個函式,然後再執行你的程式碼,這樣的過程就是 Middleware。

房地產仲介幫助你找到適合的房子,就像 Middleware 在處理請求時,執行一些額外的操作,然後將處理權交給下一個處理或路由。

整個 Express 其實都是由 Middleware 的概念所組成的,因此 Middleware 是非常重要的一個概念。

回頭來講一下 app.use(express.json()); 這個 Middleware,前面有提到這個 Middleware 會幫助我們解析 req.body,讓我們可以正常取得資料,這邊讓我們來看一下它的原始碼:

1
2
3
4
// express/lib/express.js
var bodyParser = require('body-parser')
// ... 略過其他程式碼
exports.json = bodyParser.json

我們可以看到 app.use(express.json()); 其實就是 app.use(bodyParser.json());,只是被 Express 給封裝起來了而已,接著我們再往 body-parser 中看一下 json 的原始碼:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// body-parser/lib/types/json.js

// ... 略過其他程式碼

function json (options) {
var opts = options || {}

var limit = typeof opts.limit !== 'number'
? bytes.parse(opts.limit || '100kb')
: opts.limit
var inflate = opts.inflate !== false
var reviver = opts.reviver
var strict = opts.strict !== false
var type = opts.type || 'application/json'
var verify = opts.verify || false

if (verify !== false && typeof verify !== 'function') {
throw new TypeError('option verify must be function')
}

// create the appropriate type checking function
var shouldParse = typeof type !== 'function'
? typeChecker(type)
: type

function parse (body) {
if (body.length === 0) {
// special-case empty json body, as it's a common client-side mistake
// TODO: maybe make this configurable or part of "strict" option
return {}
}

if (strict) {
var first = firstchar(body)

if (first !== '{' && first !== '[') {
debug('strict violation')
throw createStrictSyntaxError(body, first)
}
}

try {
debug('parse json')
return JSON.parse(body, reviver)
} catch (e) {
throw normalizeJsonSyntaxError(e, {
message: e.message,
stack: e.stack
})
}
}

return function jsonParser (req, res, next) {
if (req._body) {
debug('body already parsed')
next()
return
}

req.body = req.body || {}

// skip requests without bodies
if (!typeis.hasBody(req)) {
debug('skip empty body')
next()
return
}

debug('content-type %j', req.headers['content-type'])

// determine if request should be parsed
if (!shouldParse(req)) {
debug('skip parsing')
next()
return
}

// assert charset per RFC 7159 sec 8.1
var charset = getCharset(req) || 'utf-8'
if (charset.slice(0, 4) !== 'utf-') {
debug('invalid charset')
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
charset: charset,
type: 'charset.unsupported'
}))
return
}

// read
read(req, res, next, parse, debug, {
encoding: charset,
inflate: inflate,
limit: limit,
verify: verify
})
}
}

// ... 略過其他程式碼

這邊很複雜沒有錯,但真正整個核心是在 return function jsonParser (req, res, next) { ... } 這個函式,這個函式會在 req 之前執行,這過程中,就會依照設定來解析,例如:limitinflatereviverstricttypeverify 等等,當解析完畢後,就會將解析後的 JSON 資料附加到請求物件的 body 屬性中供後續路由處理函式使用。

看完一個超級複雜版本的 Middleware 後,我們來試著自己的 Middleware 吧?

自己的 Middleware

建立自己的 Middleware 其實並不困難,你只需要建立一個函式,並且在函式中呼叫 next() 就可以了

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

const app = express();

const myMiddleware = (req, res, next) => {
console.log('myMiddleware!');
next();
};

app.use(myMiddleware);

app.get('/', (req, res) => {
res.send('Hello, World!');
});

app.listen(3000)

是不是很簡單呢?當你試著戳 http://localhost:3000 時,你會發現終端機中會印出 myMiddleware!

透過這個概念,以及總結前面的知識,其實我們是可以自己寫一個 JSON 的解析。

我們純 Node.js 的時候,其實是可以透過 req.on('data', (chunk) => { ... }) 來解析 JSON 的,因此我們可以透過這個概念來實作一個 JSON 解析的 Middleware

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
const express = require('express');

const app = express();

const jsonParserMiddleware = (req, res, next) => {
let data = '';

// 監聽數據流事件,並將數據流串接起來
req.on('data', (chunk) => {
data += chunk.toString();
});

// 監聽數據流結束事件,並將解析後的 JSON 資料附加到請求物件的 body 屬性
req.on('end', () => {
try {
// 將解析後的 JSON 資料附加到請求物件的 body 屬性
req.body = JSON.parse(data);

// 繼續執行後續的 Middleware 或路由處理函式
next();
} catch (error) {
// JSON 解析失敗,回傳錯誤訊息
res.status(400).json({ error: 'Invalid JSON data' });
}
});
}

app.use(jsonParserMiddleware);

app.post('/', (req, res) => {
res.send(req.body);
});

app.listen(3000)

透過自己所建立的 Middleware 我們就可以在 Post / 中取得 req.body 囉~

當然 Middleware 不只有可以做解析而已,在實戰上通常會搭配在身份驗證上,例如像是 Token 是否有過期、是否有權限等等,這些都可以透過 Middleware 來處理。

那麼這一篇也差不多要告一個段落了,所以最後我們就來總結一下 Middleware 的概念吧!

  • Middleware 是一個函式
  • Middleware 會在路由處理函式之前執行
  • Middleware 必須傳入 reqresnext 這三個參數
  • Middleware 必須呼叫 next() 才能繼續執行後續的動作
  • Middleware 可以用來做身份驗證、解析資料、處理錯誤等等

那麼這一篇我們就先到這邊結束囉~

碎碎念

前陣子使用 Nuxt3 的時候踩雷踩到快崩潰,還好 ChatGPT 救了我?!

你的支持會直接轉換成更多技術筆記

如果我的筆記讓你少踩一個坑、節省 Debug 的時間,
也許你可以請我喝杯咖啡,讓我繼續當個不是 Array 的 Ray ☕

buymeacoffee | line | portaly
Terminal

整理這些技術筆記真的很花時間,如果你願意 關閉 Adblock 支持我,我會把這份感謝轉換成更多「踩坑轉避坑」的內容給你!ヽ(・∀・)ノ

Advertisement

分享這篇文章

留言

© 2026 Ray. All rights reserved.

Powered by Ray Theme