花 15 分鐘把 Express.js 搞明白,全棧沒有那么難
大家好,我是楊成功。
Express 是老牌的 Node.js 框架,以簡單和輕量著稱,幾行代碼就可以啟動一個 HTTP 服務(wù)器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都與 Express 息息相關(guān)。
Express 框架使用標準 Node.js 語法,主要由以下 3 個核心部分組成:
- 路由。
- 中間件。
- 錯誤處理。
認識基本結(jié)構(gòu)
Express 的基本結(jié)構(gòu)很簡單,只需要三行代碼,應(yīng)用就可以運行起來。
const express = require('express')
const app = express()
app.listen(9000, () => console.log('啟動成功'))
假設(shè)上述代碼寫在 index.js 中,我們啟動該應(yīng)用使用命令 node ./index.js,控制臺會輸出“啟動成功”。
為了方便,我們也可以在 package.json 中創(chuàng)建快捷命令,如下:
// package.json
{
"scripts": {
"start": "node ./index.js"
}
}
那么現(xiàn)在啟動應(yīng)用就可以用 npm run start 命令。
不過這種方式在本地運行項目時會有一個弊端,就是修改文件后不會立即生效,需要重新啟動。為了提高效率,一般會使用一個名為 PM2 的模塊啟動 Node.js 應(yīng)用。
首先全局安裝 pm2:
$ npm install -g pm2
安裝后在項目目錄下創(chuàng)建啟動配置文件 ecosystem.config.js,代碼如下:
module.exports = {
apps: [
{
name: 'first-api',
script: './index.js',
},
],
}
然后在項目目錄下執(zhí)行以下命令就可以啟動項目了:
$ pm2 start --watch
上圖中的 0
就是啟動應(yīng)用的 ID,下面會用到。
PM2 常用大命令如下:
- pm2 start:啟動應(yīng)用,--watch 表示監(jiān)聽文件修改自動重啟。
- pm2 list:查看已啟動的應(yīng)用列表。
- pm2 logs <id>:查看日志輸出。
- pm2 delete <id>:刪除指定應(yīng)用。
應(yīng)用啟動后監(jiān)聽 9000 端口,但訪問 “http://localhost:9000” 卻沒有反應(yīng),請求被掛起,這是因為沒有設(shè)置如何處理請求。
Express 中通過定義路由來處理請求。
使用路由創(chuàng)建 API 接口
路由用于定義如何處理請求,定義方式采用以下結(jié)構(gòu):
app.METHOD(PATH, HANDLER)
其中 app 表示 Express 的實例,其余的三個部分都屬于路由配置,表示的含義如下:
- METHOD:路由方法。
- PATH:路由地址。
- HANDLER:路由處理函數(shù)。
比如示例代碼中的路由是這樣子:
app.get('/', (req, res) => {
res.send('Hello World')
})
使用app.get()定義了一個 GET 請求的路由,第一個參數(shù) “/” 為路由地址,第二個參數(shù)為路由處理函數(shù),是一個回調(diào)函數(shù),該函數(shù)接受兩個參數(shù)分別表示請求和響應(yīng)。
當路由方法和路由地址匹配到用戶請求時,路由處理函數(shù)就會執(zhí)行。
路由方法根據(jù)基本 API 規(guī)則支持五種,分別如下:
- app.get():GET 請求。
- app.post():POST 請求。
- app.put():PUT 請求。
- app.delete():DELETE 請求。
- app.all():匹配所有請求。
以上五個方法的參數(shù)都與示例路由一致。定義好路由后,我們的主要任務(wù)是在路由處理函數(shù)中編寫業(yè)務(wù)代碼,一般會包括接收請求參數(shù)、返回接口響應(yīng),這里要用到路由處理函數(shù)的兩個參數(shù)。
請求對象
路由處理函數(shù)的第一個參數(shù)表示請求對象,包含客戶端請求攜帶的相關(guān)數(shù)據(jù),常用的屬性如下:
- req.query:URL 附加參數(shù)。
- req.body:請求體參數(shù)。
- req.method:請求方法。
- req.headers:請求頭對象。
- req.params:URL 地址參數(shù)。
現(xiàn)在我們定義一個路由,將請求對象的這幾個屬性返回,看一下它們的值是什么:
app.post('/first/:id', (req, res) => {
let { method, query, body, params, headers } = req
res.send({ method, query, body, params, headers })
})
在 Postman 中請求地址 “http://localhost:9000/first/8?tag=test” 并傳入請求體參數(shù) {data: "xxx"},請求結(jié)果如下:
對照請求參數(shù)和返回結(jié)果,可以發(fā)現(xiàn)路由地址中的 :id 占位符解析后被放到 “req.params” 對象下。地址參數(shù) ?tag=test 解析后被放到 “req.query” 對象下。
但是有一個問題:請求體沒有被解析出來。
這是因為請求體是按照流處理的,無法直接獲取到,我們需要一個第三方工具包協(xié)助。首先安裝如下:
$ yarn add body-parser
然后在 index.js 中引入并加載:
const bodyParser = require('body-parser')
app.use(bodyParser.json())
現(xiàn)在重新請求,接可以看到 req.body 的返回結(jié)果了:
響應(yīng)對象
路由處理函數(shù)的第二個參數(shù)表示響應(yīng)對象,用于向客戶端返回結(jié)果,也就是定義接口的返回值。路由處理函數(shù)中必須設(shè)置響應(yīng),否則客戶端請求會一直處于掛起狀態(tài),無返回值。
常用的響應(yīng)方法有以下三種,用于返回不同類型的數(shù)據(jù):
- res.json():發(fā)送 JSON 響應(yīng)。
- res.render():發(fā)送視圖響應(yīng)(HTML)。
- res.send():發(fā)送各種類型的響應(yīng)。
我們統(tǒng)一使用 res.send() 方法響應(yīng)數(shù)據(jù)。一般在響應(yīng)前還可以通過 res.status() 方法設(shè)置 HTTP 狀態(tài)碼,示例如下:
res.send('哈哈') // 狀態(tài)碼:200,返回值:"哈哈"
res.status(201).send({
msg: 'created',
}) // 狀態(tài)碼:201,返回值:{msg:"created"}
res.status(401).send('請登錄') // 狀態(tài)碼:401,返回值:"請登錄"
發(fā)送響應(yīng)時也常常會遇到問題,以下兩條原則請牢記,避免踩坑:
- 一個路由處理函數(shù)中只能響應(yīng)一次,不能重復(fù)響應(yīng)。
- res.send() 不能直接返回數(shù)字。
分組路由
使用 app 實例注冊路由固然方便,但是如果定義的路由很多,都注冊在 app 實例下很可能會帶來全局污染,這與全局變量一個道理。為了應(yīng)用的健壯性,我們應(yīng)該將路由分組。
Express 提供了 Router 類來創(chuàng)建模塊化的路由程序,它像一個微應(yīng)用,可以隨時被 app 實例掛載。這樣就可以把一組路由保存在一個單獨的文件中,需要時加載,從而實現(xiàn)路由分組。
創(chuàng)建一個 router 文件夾用于保存路由文件,然后創(chuàng)建 router/test.js 文件,在文件中呢寫入路由代碼,如下:
// router/test.js
var express = require('express')
var router = express.Router()
router.post('/info', (req, res) => {
res.send('TEST 路由組')
})
module.exports = router
這樣一個基本的路由模塊就寫好了,如果讓其生效,需要在主程序中加載該模塊:
const testRouter = require('./router/test.js')
app.use('/test', testRouter)
上述代碼表示請求 “/test” 時加載路由模塊,訪問某個路由時使用該路徑拼接路由地址,像下面這樣:
http://localhost:9000/test/info
# 返回 "TEST 路由組"
為了開發(fā)規(guī)范,我們統(tǒng)一把路由定義為路由模塊,而不直接在 app 下注冊。
理解中間件,搞懂框架原理
Express 應(yīng)用是由一系列中間件構(gòu)成的。中間件同樣是一個聽著很玄乎的詞兒,但它的本質(zhì)就是一個函數(shù)。我們看一個中間件函數(shù)的代碼示例:
var myLogger = function (req, res, next) {
console.log('LOGGED')
next()
}
中間件與普通函數(shù)的區(qū)別就是它有三個參數(shù),分別表示請求對象(req),響應(yīng)對象(res)和一個 next() 函數(shù) ——— 也許你發(fā)現(xiàn)了,路由處理函數(shù)也是這個結(jié)構(gòu)。
沒錯,路由處理函數(shù)本身就是一個中間件。
將中間件掛載到應(yīng)用上,使用 app.use() 方法:
app.use(myLogger)
看到這里你又會發(fā)現(xiàn),請求體解析包 body-parser 也是這么掛載的,因為該包也是一個中間件。
直接用 app.use() 掛載的中間件在收到任意請求時都會執(zhí)行。如果要限定執(zhí)行條件,可以添加一個路徑匹配,如下:
app.use('/test/*', myLogger)
這樣,只有以 /test 開頭的請求才會執(zhí)行 myLogger 中間件,這看起來與路由注冊很相似。其實注冊路由正是這種中間件掛載方式的快捷寫法,只不過多了一個請求方法的限制。
Express 應(yīng)用中一切皆中間件,如果匹配到多個中間件會按照順序依次調(diào)用。此時 next() 函數(shù)就能派上用場了,他的作用是進入下一個中間件。
比如代碼中的 myLogger 中間件,將它掛載到路由之前,那么每次請求首先會打印出 “LOGGED”,然后再進入路由處理函數(shù)。
如果 myLogger 中間件中沒有調(diào)用 next() 函數(shù),請求就會被堵在這里,無法進入路由處理函數(shù),此時請求會被掛起。
統(tǒng)一錯誤處理,提升健壯性
既然一切皆中間件,那么錯誤處理也是一個中間件。錯誤處理函數(shù)與其他的中間件函數(shù)稍有不同,它多了一個 err 參數(shù),如下:
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('服務(wù)器出錯了!')
})
err 參數(shù)表示錯誤信息,當發(fā)生錯誤時進入該中間件,此時要設(shè)置 HTTP 狀態(tài)碼為 500,并根據(jù)錯誤信息為客戶端返回錯誤響應(yīng)。
錯誤處理中間件是一個兜底中間件,請確保它定義在所有中間件之后,是應(yīng)用中的最后一個中間件。
請求進入錯誤中間件,說明前面的所有中間件都沒有匹配到。但是如果客戶端請求地址寫錯而進入錯誤處理中間件,此時返回 500 錯誤顯然不合理,應(yīng)該是 404 資源未找到。
所以在錯誤處理中間件前,還應(yīng)該定義一個 404 中間件。該中間件要在所有路由之后,錯誤處理之前,是應(yīng)用的倒數(shù)第二個中間件,代碼如下:
app.use((req, res, next) => {
res.status(404).send('Not Found')
})
好了,現(xiàn)在我們的應(yīng)用就健壯多了。
總結(jié)
本文列舉了 Express 框架的核心,并舉例如何應(yīng)用,整體并沒有那么難。掌握這部分知識,可以快速擁 API 開發(fā)的思維。