Day7 - Web Framework to Express.js

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 救了我?!