根據源碼模擬實現express框架經常使用功能

1 express的一些經常使用功能

咱們在開發的過程當中,或多或少會用到Node.js,好比用Node.js在本地起一個靜態文件服務器等。可是Node.js 的 API 對開發者來講並非很是友好。例如,若是咱們想從服務器發送一個 JPEG 圖片的話,可能須要至少 四五 行代碼才行。建立可複用 HTML 模版則更復雜。另外,Node.js 的 HTTP 模塊雖然強大,可是仍然缺乏一些實用特性。 Express 的出現就是爲了解決這些問題,讓咱們可以高效的使用 Node.js 來編寫 Web 應用。javascript

從大的方面來講,Express 爲 Node.js 的 HTTP 模塊帶來了兩大特性:html

  • 經過提供大量易用接口,簡化了程序的複雜度。
  • 它容許對請求處理函數進行拆分,將其重構爲不少負責特定請求的小型請求處理函數。便於模塊化和後期維護。

下面咱們說幾個express的幾個經常使用api:java

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

根據請求路徑來處理客戶端發出的GET等各類請求。第一個參數path爲請求的路徑, 第二個參數爲處理請求的回調函數。git

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請求的函數,用來完成各類特定的任務,好比檢查用戶是否登陸、檢測用戶是否有權限訪問等。github

app.use中放入的函數稱爲中間件函數,通常有三個特色:express

  • 一箇中間件處理完請求和響應能夠把相應數據再傳遞給下一個中間件。
  • 回調函數的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)

監聽客戶端向服務器發送請求的函數api

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)

設置參數,好比渲染模板的時候咱們會常用到。bash

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中的每一層(如圖中的layer一、layer二、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

參考文獻

相關文章
相關標籤/搜索