根據原始碼模擬實現express框架常用功能

whynotgonow發表於2018-03-10

1 express的一些常用功能

我們在開發的過程中,或多或少會用到Node.js,比如用Node.js在本地起一個靜態檔案伺服器等。但是Node.js 的 API 對開發者來說並不是非常友好。例如,如果我們想從伺服器傳送一個 JPEG 圖片的話,可能需要至少 四五 行程式碼才行。建立可複用 HTML 模版則更復雜。另外,Node.js 的 HTTP 模組雖然強大,但是仍然缺少一些實用特性。 Express 的出現就是為了解決這些問題,讓我們能夠高效的使用 Node.js 來編寫 Web 應用。

從大的方面來說,Express 為 Node.js 的 HTTP 模組帶來了兩大特性:

  • 通過提供大量易用介面,簡化了程式的複雜度。
  • 它允許對請求處理函式進行拆分,將其重構為很多負責特定請求的小型請求處理函式。便於模組化和後期維護。

下面我們說幾個express的幾個常用api:

1) app[method](path, function(req, res){})

根據請求路徑來處理客戶端發出的GET等各種請求。第一個引數path為請求的路徑, 第二個引數為處理請求的回撥函式。

let express = require('express');
let app = express();
app.listen(8080, () => {
    console.log('started success');
});
app.get('/', function (req, res) {
    res.end('ok');
});
複製程式碼

2) app.use([path], function(req, res){})

中介軟體就是處理HTTP請求的函式,用來完成各種特定的任務,比如檢查使用者是否登入、檢測使用者是否有許可權訪問等。

app.use中放入的函式稱為中介軟體函式,一般有三個特點:

  • 一箇中介軟體處理完請求和響應可以把相應資料再傳遞給下一個中介軟體。
  • 回撥函式的next引數,表示接受其他中介軟體的呼叫,函式體中的next(),表示將請求資料繼續傳遞。
  • 可以根據路徑來區分返回執行不同的中介軟體。
const express = require('../lib/express');
const app = express();

app.use(function (req, res, next) {
    console.log('Ware1:', Date.now());
    next('wrong');
});
app.get('/', function (req, res, next) {
    res.end('1');
});
const user = express.Router();
user.use(function (req, res, next) {
    console.log('Ware2', Date.now());
    next();
});
user.use('/2', function (req, res, next) {
    res.end('2');
});
app.use('/user', user);
app.use(function (err, req, res, next) {
    res.end('catch ' + err);
});
app.listen(3000, function () {
    console.log('server started at port 3000');
});
複製程式碼

3) app.listen(port, callback)

監聽客戶端向伺服器傳送請求的函式

4) app.param(paramName, callback)

批量處理相同引數

const express = require('../lib/express');
const app = express();
app.param('uid',function(req,res,next,val,name){
    req.user = {id:1,name:'Lucy'};
    console.log('1');
    next();
})
app.param('uid',function(req,res,next,val,name){
    req.user.name = 'Tom';
    next();
})
app.get('/user/:uid',function(req,res){
    console.log(req.user);
    res.end('user');
});
app.listen(3000);
複製程式碼

5) app.set(key, val)

設定引數,比如渲染模板的時候我們會經常使用到。

app.set('views', path.resolve(path.join(__dirname, 'views')));
app.set('view engine', 'html');
複製程式碼

6) app.engine()

規定何種檔案用何種方法來渲染

app.engine('html', html);
複製程式碼

簡單的介紹了集中常用api的用法,接下來就要開始進入主題了,那就是根據express原始碼,模擬express框架,實現上述的集中api。

2 實現express的邏輯圖和相應的介紹

本次模擬實現的api有app.get()app.use()app.listen()app.param()app.render()app.set()app.engine()

專案結構如下:

lib/
|
| - middle/
|   | - init.js     內建中介軟體
|
| - route/
|   | - index.js    路由系統
|   | - layer.js    層
|   | - route.js    路由
|
| - application.js  應用
| - html.js         模板引擎
| - express.js      入口
|
test/               這裡放入的是測試用例
|
複製程式碼

接下來我們一一介紹一下express的實現邏輯。因為express都是通過app來操作的,即express.js檔案是express的入口,express.js的程式碼實現很簡單,就是匯出一個Application的例項。express把主要的方法放在Application上面了,我們先來張Application的概覽圖,來直觀的感受下,如下圖:

Application的例項
紫色邊框左側的一欄文字是Application上的屬性,黑顏色的部分是例項上的屬性,紅顏色加粗的部分是原型上的屬性,下面的圖也遵循相同的規則。我們詳細說明一下他們:

Application上的屬性

  • settings - 儲存設定的引數
  • engines - 儲存副檔名和相對應的渲染函式的函式
  • _router - 是一個Router的例項(圖中箭頭指向的灰色背景部分),後面我們會詳細介紹
  • set - 設定引數的方法
  • engine - 設定模板引擎
  • render - 渲染模板引擎
  • lazyRouter - 懶載入_router屬性
  • [method] - 路由
  • use - 中介軟體
  • param - 批量設定相同的引數
  • listen - 監聽客戶端發來請求的函式

為了便於描述我們將Application的例項稱為app(下同)。app是實現express功能的入口,順著圖中第一個箭頭的方向,app._router屬性指向一個Router的例項(灰色背景部分),app._router是一個路由系統,這個路由系統中會管理客戶端發來請求的回撥函式的執行。Router上的屬性也位於左側的一欄文字中,我們先來解釋一下屬性(同樣的,黑色部分為例項上的屬性,紅色加粗部分為原型上的屬性)。

Router上的屬性

  • stack - 指向的是一個陣列(黑色邊框),裡面存放的是一層層的Layer的例項(Layer下面接著會介紹)
  • paramCallbacks - 存放的是處理引數的函式
  • route - 返回一個路由例項
  • process_params - 處理匹配到的引數
  • handle - 處理客戶端發來的請求
  • param - 訂閱我們引數處理函式
  • use - 訂閱中介軟體函式
  • [method] - 訂閱路由函式

在我們處理客戶端發來請求的回撥函式的過程中,主要靠的是迴圈app._router.stack中的每一層(如圖中的layer1、layer2、layer3)來實現,那麼每一層到底是什麼呢?我個人根據處理的邏輯把layer做了一個分類,包括三類:路由層、中介軟體層、具有子路由系統的中介軟體層。我們詳細介紹一下這三個類:

1) 路由層

路由層

Layer上的屬性

  • path - 路由的路徑,如/user/getlist
  • route - 返回一個Route的例項
  • handler - 新建例項時傳入的一個函式
  • keys - 路由引數的key組成的陣列
  • regexp - 匹配路由引數的正則物件
  • params - 存放的是匹配到的路由引數
  • match - 匹配當前例項上的path是否和請求的url地址匹配
  • handle_request - 執行本層this.handler屬性對應的方法
  • handle_error - 處理上一層next()函式傳來的引數

路由層是通過app.get(path, handler)訂閱的,該層會通過app._router.stack.push()放入到app._router.stack中,app._router.stack是一個陣列,存放的是各種層(layer),包括後面的中介軟體層也會放到app._router.stack中。需要注意的是路由層的route屬性指向是一個Route的例項,並且在new Layer的時候將Route的例項上的dispatch方法作為第二個引數傳遞給Layer,如下程式碼:

let route = new Route(path);
let layer = new Layer(path, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
複製程式碼

需要注意的是路由層的route屬性指向一個Route的例項。

Route的屬性

  • path - 統一傳入一個'/'
  • stack - 也是一個陣列,存放的也是一個個的層(layer),如圖中的layer_a、 layer_b、layer_c,並且這裡的層跟路由層的原型指向同一個建構函式。但是這裡的layer的handler屬性指的是app.get(path, handlers)中handlers中的單個handler。app._router.stack中的layer的handler屬性指向的是route.dispatch.bind(route)
  • method - 是一個物件,存放的是訂閱到stack中的方法的集合
  • handle_method - 檢測本route是否存在請求中的方法
  • dispatch - 路由層的handler是派發到這裡
  • [method] - 因為路由層上的[method]方法最終是派發給route中來實現,所以這個方法就是將派發來的[method]方法pushstack
2) 中介軟體層

中介軟體層
中介軟體層是通過app.use(path, handler)訂閱的,該層也會放入到app._router.stack中。需要注意的是該層的route屬性為undefined

3) 具有子路由系統的中介軟體層

具有子路由系統的中介軟體層
該層也是一箇中介軟體層,只是具有獨立的子路由系統,這個子路由系統跟上面app._router所屬的類是同一個類,所以這個子路由系統跟app._router具有相同的屬性和相同的原型上的方法。這一層也是通過app.use()訂閱的,但是稍有不同,如下程式碼:

//中介軟體層 的訂閱方式
app.use('/', function (req, res, next) {
    console.log('Ware1:', Date.now());
    next('wrong');
});

// 具有子路由系統的中介軟體層 的訂閱方式
const user = express.Router();
user.use(function (req, res, next) {
    console.log('Ware2', Date.now());
    next();
});
user.use('/2', function (req, res, next) {
    res.end('2');
});
複製程式碼

當請求函式走到這一層的時候,this.handler執行時會進入到圖中箭頭指向的灰色背景部分,即子路由系統,這個子路由系統中的stack也是存放的是子路由系統中訂閱的函式。

介紹了這麼多,到底這些RouterLayerRoute等是如何配合工作的?下面我們詳細介紹一下。

3 客戶端發起請求時的執行邏輯順序

當客戶端發起請求的時候app就會派發給_router.handle執行,_router.handle的邏輯就是把訂閱在_router.stack中的handler依次執行,如下圖:

router.stack執行的順序圖

接下來我把_router.stack裡面每一個layer時的執行書序邏輯圖抽離出來,如下圖:

執行的邏輯圖

4 實現模板引擎

express還有一個功能就是可以實現模板引擎,實現的程式碼邏輯如下:

let head = "let tpl = ``;\nwith (obj) {\n tpl+=`";
str = str.replace(/<%=([\s\S]+?)%>/g, function () {
    return "${" + arguments[1] + "}";
});
str = str.replace(/<%([\s\S]+?)%>/g, function () {
    return "`;\n" + arguments[1] + "\n;tpl+=`";
});
let tail = "`}\n return tpl; ";
let html = head + str + tail;
let fn = new Function('obj', html);
let result = fn(options);
複製程式碼

5 寫在最後

寫到這裡,express框架的常用api已經介紹完了,本文只是介紹了實現邏輯,具體的專案程式碼以及測試用例請參見我的GitHub

參考文獻

相關文章