Node - Express 的實現(超詳細)
序:
因為公司 Node
方面業務都是基於一個小型框架寫的,這個框架是公司之前的一位同事根據 Express
的中介軟體思想寫的一個小型 Socket
框架,閱讀其原始碼之後,對 Express
的中介軟體思想有了更深入的瞭解,接下來就手寫一個 Express
框架 ,以作為學習的產出 。
在閱讀了同事的程式碼與 Express
原始碼之後,發現其實 Express
的核心就是中介軟體的思想,其次是封裝了更豐富的 API
供我們使用,廢話不多說,讓我們來一步一步實現一個可用的 Express
。
本文的目的在於驗證學習的收穫,大概細緻劃分如下:
- 伺服器監聽的原理
- 路由的解析與匹配
- 中介軟體的定義與使用
- 核心
next()
方法 - 錯誤處理中介軟體定義與使用
- 內建
API
的封裝
正文:
在手寫框架之前,我們有必要去回顧一下 Express
的簡單使用,從而對照它給我們提供的 API
去實現其相應的功能:
新建一個 app.js
檔案,新增如下程式碼:
// app.js
let express = require('express');
let app = express();
app.listen(3000,function () {
console.log('listen 3000 port ...')
})
複製程式碼
現在,在命令列中執行:
node app.js
複製程式碼
可以看到,程式已經在我們的後臺跑起來了。
當我們為其新增一個路由:
let express = require('Express');
let app = express();
app.get('/hello',function (req,res) {
res.setHeader('Content-Type','text/html; charset=utf-8')
res.end('我是新新增的路由,只有 get 方法才可以訪問到我 ~')
})
app.listen(3000,function () {
console.log('listen 3000 port ...' )
})
複製程式碼
再次重啟:在命令列中執行啟動命令:(每次修改程式碼都需要重新執行指令碼)並訪問瀏覽器本地 3000 埠:
這裡的亂碼是因為:伺服器不知道你要怎樣去解析輸出,所以我們需要指定響應頭:
let express = require('Express');
let app = express();
app.get('/hello','text/html; charset=utf-8') // 指定 utf-8
res.end('我是新新增的路由,只有 get 方法才可以訪問到我 ~')
})
app.post('/hi',res) {
res.end('我是新新增的路由,只有 post 方法才可以訪問到我 ~')
})
app.listen(3000,function () {
console.log('listen 3000 port ...')
})
複製程式碼
我們先來實現上面的功能:
1. 伺服器監聽的原理
新建一個 MyExpress.js
,定義一個入口函式:
let http = require('http');
function createApplication () {
// 定義入口函式,初始化操作
let app = function (req,res) {
}
// 定義監聽方法
app.listen = function () {
// 通過 http 模組建立一個伺服器例項,該例項的引數是一個函式,該函式有兩個引數,分別是 req 請求物件和 res 響應物件
let server = http.createServer(app);
// 將引數列表傳入,為例項監聽配置項
server.listen(...arguments);
}
// 返回該函式
return app
}
module.exports = createApplication;
複製程式碼
現在,我們程式碼中的 app.listen()
其實就已經實現了,可以將引入的 express
替換為我們寫的 MyExpress
做驗證:
let express = require('Express');
// 替換為
let express = require('./MyExpress');
複製程式碼
2. 路由的解析與匹配:
接下來,
我們先看看 routes
中的原理圖
根據上圖,路由陣列中存在多個 layer
層,每個 layer
中包含了三個屬性, method
、path
、handler
分別對應請求的方式、請求的路徑、執行的回撥函式,程式碼如下:
const http = require('http')
function createApp () {
let app = function (req,res) {
};
app.routes = []; // 定義路由陣列
let methods = http.METHODS; // 獲取所有請求方法,比如常見的 GET/POST/DELETE/PUT ...
methods.forEach(method => {
method = method.toLocaleLowerCase() // 小寫轉換
app[method] = function (path,handler) {
let layer = {
method,path,handler,}
// 將每一個請求儲存到路由陣列中
app.routes.push(layer)
}
})
// 定義監聽的方法
app.listen = function () {
let server = http.createServer(app);
server.listen(...arguments)
}
return app;
}
module.exports = createApp
複製程式碼
到這裡,仔細思考下,當指令碼啟動時,我們把所有的路由都儲存到了 routes
,列印 routes
,可以看到:
是不是和我們上面圖中的一模一樣 ~
此時,我們訪問對應的路徑,發現瀏覽器一直轉圈圈,這是因為我們只是完成了存的操作,把所有的 layer
層存到了 routes
中。
那麼我們該如何才可以做的當訪問的時候,呼叫對應的 handle
函式呢?
思路:當我們訪問路徑時,也就是獲取到請求物件 req
時,我們需要遍歷所存入的 layer
與訪問的 method
、path
進行匹配,匹配成功,則執行對應的 handler
函式
程式碼如下:
const url = require('url')
......
let app = function (req,res) {
let reqMethod = req.method.toLocaleLowerCase() // 獲取請求方法
let pathName = url.parse(req.url,true).pathname // 獲取請求路徑
console.log(app.routes);
app.routes.forEach(layer => {
let { method,handler } = layer;
if (method === reqMethod && path === pathName) {
handler(req,res)
}
});
};
......
複製程式碼
至此,路由的定義與解析也基本完成。
3. 中介軟體的定義與使用
接下來,就是重點了,中介軟體思想。
中介軟體的定義其實與路由的定義差不多,也是存在 routes
中,但是,必須放到所有路由的 layer
之前,原理如下圖:
其中,middle1
、middle2
、middle3
都是中介軟體,middle3 放在最後面,一般作為錯誤處理中介軟體,並且,每次訪問伺服器的時候,所有的請求先要經過 middle1
、middle2
做處理。
在中介軟體中,有一個 next
方法,其實 next
方法就是使 ** layer
** 的 index
標誌向後移一位,並進行匹配,匹配成功執行回撥,匹配失敗則繼續向後匹配,有點像 ** 回撥佇列**。
核心 next()
方法
接下來我們實現一個 next
方法:
因為只有中介軟體的回撥中才具有 next
方法,但是我們的中介軟體和路由的 layer
層都是存在 routes
中的,所以首先要判斷 layer
中的 method
是否為 middle
初次之外,還要判斷,中介軟體的路由是否相匹配,因為有些中介軟體是針對某個路由的。
let reqMethod = req.method.toLocaleLowerCase()
let pathName = url.parse(req.url,true).pathname
let index = 0;
function next () {
// 中介軟體處理
if (method === 'middle') {
// 檢測 path 是否匹配
if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
handler(req,res,next) // 執行中介軟體回撥
} else {
next()
}
// 路由處理
} else {
// 檢測 method 與 path 是否匹配
if (method === reqMethod && path === pathName) {
handler(req,res) // 執行路由回撥
} else {
next()
}
}
}
next() // 這裡必須要呼叫一次 next ,意義在於初始化的時候,取到第一個 layer,
複製程式碼
如果遍歷完 routes
,都沒有匹配的 layer
,該怎麼辦呢?所以要在 next
方法最先判斷是否邊已經遍歷完:
function next () {
// 判斷是否遍歷完
if (app.routes.length === index) {
return res.end(`Cannot ${reqMethod} ${pathName}`)
}
let { method,handler } = app.routes[index++];
// 中介軟體處理
if (method === 'middle') {
if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
handler(req,next)
} else {
next()
}
} else {
// 路由處理
if (method === reqMethod && path === pathName) {
handler(req,res)
} else {
next()
}
}
}
next()
複製程式碼
這樣,一個 next 方法功能基本完成了。
4. 錯誤處理中介軟體定義與使用
如上面圖中所示,錯誤處理中介軟體放在最後,就像一個流水線工廠,錯誤處理就是最後一道工序,但並不是所有的產品都需要跑最後一道工序,就像:只有不合格的產品,才會進入最後一道工序,並被貼上不合格的標籤,以及不合格的原因。
我們先看看 Express
中的錯誤是怎麼被處理的:
// 中介軟體1
app.use(function (req,next) {
res.setHeader('Content-Type','text/html; charset=utf-8')
console.log('middle1')
next('這是錯誤')
})
// 中介軟體2
app.use(function (req,next) {
console.log('middle2')
next()
})
// 中介軟體3(錯誤處理)
app.use(function (err,req,next) {
if (err) {
res.end(err)
}
next()
})
複製程式碼
如上圖所示:有三個中介軟體,當 next
方法中丟擲錯誤時,會把錯誤當做引數傳入 next
方法,然後,next
指向的下一個方法就是錯誤處理的回撥函式,也就是說:next
方法中的參被當做了錯誤處理中介軟體的 handler
函式的引數傳入。程式碼如下:
function next (err) {
// 判斷是否遍歷完成
if (app.routes.length === index) {
return res.end(`Cannot ${reqMethod} ${pathName}`)
}
let { method,handler } = app.routes[index++];
if (err) {
console.log(handler.length)
// 判斷是否有 4 個引數:因為錯誤中介軟體與普通中介軟體最直觀的區別就是引數數量不同
if (handler.length === 4) {
// 錯誤處理回撥
handler(err,next)
} else {
// 一直向下傳遞
next(err)
}
} else {
// 中介軟體處理
if (method === 'middle') {
if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
handler(req,next)
} else {
next()
}
} else {
// 路由處理
if (method === reqMethod && path === pathName) {
handler(req,res)
} else {
next()
}
}
}
}
複製程式碼
麻雀雖小五臟俱全,至此,一個簡單的 Express
就完成了。你可以根據自己的興趣來封裝自己的 API
了 ...
總結:
- 中介軟體的核心是
next
方法。 -
next
方法只負責維護routes
陣列和取出layer
,根據條件去決定是否執行回撥。
附完整程式碼:
const http = require('http')
const url = require('url')
function createApp () {
let app = function (req,res) {
let reqMethod = req.method.toLocaleLowerCase()
let pathName = url.parse(req.url,true).pathname
let index = 0;
function next (err) {
if (app.routes.length === index) {
return res.end(`Cannot ${reqMethod} ${pathName}`)
}
let { method,handler } = app.routes[index++];
if (err) {
console.log(handler.length)
if (handler.length === 4) {
console.log(1)
handler(err,next)
} else {
next(err)
}
} else {
if (method === 'middle') {
if (path === '/' || pathName === path || pathName.startsWith(path + '/')) {
handler(req,next)
} else {
next()
}
} else {
if (method === reqMethod && path === pathName) {
handler(req,res)
} else {
next()
}
}
}
}
next()
};
let methods = http.METHODS;
app.routes = [];
methods.forEach(method => {
method = method.toLocaleLowerCase()
app[method] = function (path,}
app.routes.push(layer)
}
})
app.use = function (path,handler) {
if (typeof path === 'function') {
handler = path;
path = '/';
}
let layer = {
method: 'middle',path
}
app.routes.push(layer)
}
app.listen = function () {
let server = http.createServer(app);
server.listen(...arguments)
}
return app;
}
module.exports = createApp
複製程式碼