express 源碼閱讀

1. 簡介

這篇文章主要的目的是分析理解express的源碼,網絡上關於源碼的分析已經數不勝數,這篇文章準備另闢蹊徑,仿製一個express的輪子,經過測試驅動的開發方式不斷迭代,正向理解express的代碼。javascript

這篇文章中的express源碼是參考官網最新版本(v4.15.4),文章的總體思路是參考早期創做的另外一篇文章,這篇算是其升級版本。html

若是你準備經過本文學習express的基本原理,前提條件最好有必定的express使用經驗,寫過一兩個基於express的應用程序,不然對於其背後的原理理解起來難以產生共鳴,不易掌握。java

代碼連接:github.com/WangZhechao…node

2. 框架初始化

在仿製express框架前,首先完成兩件事。git

  • 確認需求。
  • 確認結構。

首先確認需求,從express的官方網站入手。網站有一個Hello world 的事例程序,想要仿製express,該程序確定須要經過測試,將改代碼複製保存到測試目錄test/index.jsgithub

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.listen(3000, function () {
  console.log('Example app listening on port 3000!')
})複製代碼

接下來,確認框架的名稱以及目錄結構。框架的名稱叫作expross。目錄結構以下:web

expross
  |
  |-- lib
  |    | 
  |    |-- expross.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js複製代碼

expross/index.js文件加載lib目錄下的expross.js文件。正則表達式

module.exports = require('./lib/expross');複製代碼

經過測試程序前兩行能夠推斷lib/expross.js導出結果應該是一個函數,因此在expross.js文件中添加以下代碼:express

function createApplication() {
  return {};
}

exports = module.exports = createApplication;複製代碼

測試程序中包含兩個函數,因此暫時將createApplication函數體實現以下:npm

function createApplication() {
    return {
        get: function() {
            console.log('expross().get function');
        },

        listen: function() {
            console.log('expross().listen function');
        }
    }
}複製代碼

雖然沒法獲得想要的結果,但至少能夠將測試程序跑通,函數的核心內容能夠在接下來的步驟中不斷完善。

至此,初始框架搭建完畢,修改test/index.js文件的前兩行:

const expross = require('../');
const app = expross();複製代碼

運行node test/index.js查看結果。

2. 第一次迭代

本節是expross的第一次迭代,主要實現的目標是將當前的測試用例功能完整實現,一共分兩部分:

  • 實現http服務器。
  • 實現get路由請求。

實現http服務器比較簡單,能夠參考nodejs官網的實現。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});複製代碼

參考該案例,實現exprosslisten函數。

listen: function(port, cb) {
    var server = http.createServer(function(req, res) {
        console.log('http.createServer...');
    });

    return server.listen(port, cb);
}複製代碼

當前listen函數包含了兩個參數,可是http.listen有不少重載函數,爲了和http.listen一致,能夠將函數設置爲http.listen的「代理」,這樣能夠保持expross().listenhttp.listen的參數統一。

listen: function(port, cb) {
    var server = http.createServer(function(req, res) {
        console.log('http.createServer...');
    });

      //代理
    return server.listen.apply(server, arguments);
}複製代碼

listen函數中,咱們攔截了全部http請求,每次http請求都會打印http.createServer ...,之因此攔截http請求,是由於expross須要分析每次http請求,根據http請求的不一樣來處理不一樣的業務邏輯。

在底層:

一個http請求主要包括請求行、請求頭和消息體,nodejs將經常使用的數據封裝爲http.IncomingMessage類,在上面的代碼中req就是該類的一個對象。

每一個http請求都會對應一個http響應。一個http響應主要包括狀態行、響應頭、消息體,nodejs將經常使用的數據封裝爲http.ServerResponse類,在上面的代碼中res就是該類的一個對象。

不只僅是nodejs,基本上全部的http服務框架都會包含request和response兩個對象,分別表明着http的請求和響應,負責服務端和瀏覽器的交互。

在上層:

服務器後臺代碼根據http請求的不一樣,綁定不一樣的邏輯。在真正的http請求來臨時,匹配這些http請求,執行與之對應的邏輯,這個過程就是web服務器基本的執行流程。

對於這些http請求的管理,有一個專有名詞 —— 「路由管理」,每一個http請求就默認爲一個路由,常見的路由區分策略包括URL、HTTP請求名詞等等,但不只僅限定這些,全部的http請求頭上的參數其實均可以進行判斷區分,例如使用user-agent字段判斷移動端。

不一樣的框架對於路由的管理規則略有不一樣,但無論怎樣,都須要一組管理http請求和業務邏輯映射的函數,測試用例中的get函數就是路由管理中的一個函數,主要負責添加get請求。

既然知道路由管理的重要,這裏就建立一個router數組,負責管理全部路由映射。參考express框架,抽象出每一個路由的基本屬性:

  • path 請求路徑,例如:/books、/books/1。
  • method 請求方法,例如:GET、POST、PUT、DELETE。
  • handle 處理函數。
var router = [{
    path: '*',
    method: '*',
    handle: function(req, res) {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('404');
    }
}];複製代碼

修改listen函數,將http請求攔截邏輯改成匹配router路由表,若是匹配成功,執行對應的handle函數,不然執行router[0].handle函數。

listen: function(port, cb) {
    var server = http.createServer(function(req, res) {

        for(var i=1,len=router.length; i<len; i++) {
            if((req.url === router[i].path || router[i].path === '*') &&
                (req.method === router[i].method || router[i].method === '*')) {
                return router[i].handle && router[i].handle(req, res);
            }
        }

        return router[0].handle && router[0].handle(req, res);
    });

    return server.listen.apply(server, arguments);
}複製代碼

實現get路由請求很是簡單,該函數主要是添加get請求路由,只須要將其信息加入到router數組便可。

get: function(path, fn) {
    router.push({
        path: path,
        method: 'GET',
        handle: fn
    });
}複製代碼

執行測試用例,報錯,提示res.send不存在。該函數並非nodejs原生函數,這裏在res上臨時添加函數send,負責發送相應到瀏覽器。

listen: function(port, cb) {
    var server = http.createServer(function(req, res) {
        if(!res.send) {
            res.send = function(body) {
                res.writeHead(200, {
                    'Content-Type': 'text/plain'
                });
                res.end(body);
            };
        }

        ......
    });

    return server.listen.apply(server, arguments);
}複製代碼

在結束此次迭代以前,拆分整理一下程序目錄:

建立application.js文件,將createApplication函數中的代碼轉移到該文件,expross.js文件只保留引用。

var app = require('./application');

function createApplication() {
    return app;
}

exports = module.exports = createApplication;複製代碼

整個目錄結構以下:

expross
  |
  |-- lib
  |    | 
  |    |-- expross.js
  |    |-- application.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js複製代碼

最後,運行node test/index.js,打開瀏覽器訪問http://127.0.0.1:3000/

3. 第二次迭代

本節是expross的第二次迭代,主要的目標是構建一個初步的路由系統。根據上一節的改動,目前的路由是用一個router數組進行描述管理,對於router的操做有兩個,分別是在application.get函數和application.listen函數,前者用於添加,後者用來處理。

按照面向對象的封裝法則,接下來將路由系統的數據和路由系統的操做封裝到一塊兒定義一個 Router類負責整個路由系統的主要工做。

var Router = function() {
    this.stack = [{
        path: '*',
        method: '*',
        handle: function(req, res) {
            res.writeHead(200, {
                'Content-Type': 'text/plain'
            });
            res.end('404');
        }
    }];
};


Router.prototype.get = function(path, fn) {
    this.stack.push({
        path: path,
        method: 'GET',
        handle: fn
    });
};


Router.prototype.handle = function(req, res) {
    for(var i=1,len=this.stack.length; i<len; i++) {
        if((req.url === this.stack[i].path || this.stack[i].path === '*') &&
            (req.method === this.stack[i].method || this.stack[i].method === '*')) {
            return this.stack[i].handle && this.stack[i].handle(req, res);
        }
    }

    return this.stack[0].handle && this.stack[0].handle(req, res);
};複製代碼

修改原有的application.js文件的內容。

var http = require('http');
var Router = require('./router');


exports = module.exports = {
    _router: new Router(),

    get: function(path, fn) {
        return this._router.get(path, fn);
    },

    listen: function(port, cb) {
        var self = this;

        var server = http.createServer(function(req, res) {
            if(!res.send) {
                res.send = function(body) {
                    res.writeHead(200, {
                        'Content-Type': 'text/plain'
                    });
                    res.end(body);
                };
            }

            return self._router.handle(req, res);
        });

        return server.listen.apply(server, arguments);
    }
};複製代碼

這樣之後路由方面的操做只和Router自己有關,與application分離,使代碼更加清晰。

這個路由系統正常運行時沒有問題的,可是若是路由不斷的增多,this.stack數組會不斷的增大,匹配的效率會不斷下降,爲了解決效率的問題,須要仔細分析路由的組成成分。

目前在expross中,一個路由是由三個部分構成:路徑、方法和處理函數。前二者的關係並非一對一的關係,而是一對多的關係,如:

GET books/1
PUT books/1
DELETE books/1複製代碼

若是將路徑同樣的路由整合成一組,顯然效率會提升不少,因而引入Layer的概念。

這裏將Router系統中this.stack數組的每一項,表明一個Layer。每一個Layer內部含有三個變量。

  • path,表示路由的路徑。
  • handle,表明路由的處理函數。
  • route,表明真正的路由。

總體結構以下圖所示:

------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| Layer     | Layer     | Layer     | Layer     |
|  |- path  |  |- path  |  |- path  |  |- path  |
|  |- handle|  |- handle|  |- handle|  |- handle|
|  |- route |  |- route |  |- route |  |- route |
------------------------------------------------
                  router 內部複製代碼

這裏先建立Layer類。

function Layer(path, fn) {
    this.handle = fn;
    this.name = fn.name || '<anonymous>';
    this.path = path;
}


//簡單處理
Layer.prototype.handle_request = function (req, res) {
  var fn = this.handle;

  if(fn) {
      fn(req, res);
  }
};


//簡單匹配
Layer.prototype.match = function (path) {
    if(path === this.path || path === '*') {
        return true;
    }

    return false;
};複製代碼

再次修改Router類。

var Router = function() {
    this.stack = [new Layer('*', function(req, res) {
        res.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        res.end('404');        
    })];
};


Router.prototype.handle = function(req, res) {
    var self = this;

    for(var i=1,len=self.stack.length; i<len; i++) {
        if(self.stack[i].match(req.url)) {
            return self.stack[i].handle_request(req, res);
        }
    }

    return self.stack[0].handle_request(req, res);
};


Router.prototype.get = function(path, fn) {
    this.stack.push(new Layer(path, fn));
};複製代碼

運行node test/index.js,訪問http://127.0.0.1:3000/一切看起來很正常,可是上面的代碼忽略了路由的屬性method。這樣的結果會致使若是測試用例存在問題:

app.put('/', function(req, res) {
    res.send('put Hello World!');
});

app.get('/', function(req, res) {
    res.send('get Hello World!');
});複製代碼

程序沒法分清PUT和GET的區別。

因此須要繼續完善路由系統中的Layer類中的route屬性,這個屬性纔是真正包含method屬性的路由。

route的結構以下:

------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| item      | item      | item      | item      |
|  |- method|  |- method|  |- method|  |- method|
|  |- handle|  |- handle|  |- handle|  |- handle|
------------------------------------------------
                  route 內部複製代碼

建立Route類。

var Route = function(path) {
    this.path = path;
    this.stack = [];

    this.methods = {};
};

Route.prototype._handles_method = function(method) {
    var name = method.toLowerCase();
    return Boolean(this.methods[name]);
};

Route.prototype.get = function(fn) {
    var layer = new Layer('/', fn);
    layer.method = 'get';

    this.methods['get'] = true;
    this.stack.push(layer);

    return this;
};

Route.prototype.dispatch = function(req, res) {
    var self = this,
        method = req.method.toLowerCase();

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(method === self.stack[i].method) {
            return self.stack[i].handle_request(req, res);
        }
    }
};複製代碼

在上面的代碼中,並無定義前面結構圖中的item對象,而是使用了Layer對象進行替代,主要是爲了方便快捷,從另外一種角度看,其實兩者是存在不少共同點的。另外,爲了利於理解,代碼中只實現了GET方法,其餘方法的代碼實現是相似的。

既然有了Route類,接下來就改修改原有的Router類,將route集成其中。

Router.prototype.handle = function(req, res) {
    var self = this,
        method = req.method;

    for(var i=0,len=self.stack.length; i<len; i++) {
        if(self.stack[i].match(req.url) && 
            self.stack[i].route && self.stack[i].route._handles_method(method)) {
            return self.stack[i].handle_request(req, res);
        }
    }

    return self.stack[0].handle_request(req, res);
};


Router.prototype.route = function route(path) {
    var route = new Route(path);

    var layer = new Layer(path, function(req, res) {
        route.dispatch(req, res);
    });

    layer.route = route;

    this.stack.push(layer);

    return route;
};


Router.prototype.get = function(path, fn) {
    var route = this.route(path);
    route.get(fn);

    return this;
};複製代碼

運行node test/index.js,一切看起來和原來同樣。

這節內容主要是建立一個完整的路由系統,並在原始代碼的基礎上引入了Layer和Route兩個概念,並修改了大量的代碼,在結束本節前總結一下目前的信息。

首先,當前程序的目錄結構以下:

expross
  |
  |-- lib
  |    | 
  |    |-- expross.js
  |    |-- application.js
  |    |-- router
  |          |
  |          |-- index.js
  |          |-- layer.js
  |          |-- route.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js複製代碼

接着,總結一下當前expross各個部分的工做。

application表明一個應用程序,expross是一個工廠類負責建立application對象。Router表明路由組件,負責應用程序的整個路由系統。組件內部由一個Layer數組構成,每一個Layer表明一組路徑相同的路由信息,具體信息存儲在Route內部,每一個Route內部也是一個Layer對象,可是Route內部的Layer和Router內部的Layer是存在必定的差別性。

  • Router內部的Layer,主要包含path、route屬性。
  • Route內部的Layer,主要包含method、handle屬性。

若是一個請求來臨,會現從頭到尾的掃描router內部的每一層,而處理每層的時候會先對比URI,相同則掃描route的每一項,匹配成功則返回具體的信息,沒有任何匹配則返回未找到。

最後,整個路由系統的結構以下:

--------------
| Application  |                                 ---------------------------------------------------------
|     |        |        ----- -----------        |     0     |     1     |     2     |     3     |  ...  |
|     |-router | ----> |     | Layer     |       ---------------------------------------------------------
 --------------        |  0  |   |-path  |       | Layer     | Layer     | Layer     | Layer     |       |
  application          |     |   |-route | ----> |  |- method|  |- method|  |- method|  |- method|  ...  |
                       |-----|-----------|       |  |- handle|  |- handle|  |- handle|  |- handle|       |
                       |     | Layer     |       ---------------------------------------------------------
                       |  1  |   |-path  |                                  route
                       |     |   |-route |       
                       |-----|-----------|       
                       |     | Layer     |
                       |  2  |   |-path  |
                       |     |   |-route |
                       |-----|-----------|
                       | ... |   ...     |
                        ----- ----------- 
                             router複製代碼

3. 第三次迭代

本節是expross的第三次迭代,主要的目標是繼續完善路由系統,主要工做有部分:

  • 豐富接口,目前只支持get接口。
  • 增長路由系統的流程控制。

當前expross框架只支持get接口,具體的接口是由expross提供的,內部調用Router.get接口,而其內部是對Route.get的封裝。

HTTP顯然不只僅只有GET這一個方法,還包括不少,例如:PUT、POST、DELETE等等,每一個方法都單獨寫一個處理函數顯然是冗餘的,由於函數的內容除了和函數名相關外,其餘都是一成不變的。根據JavaScript腳本語言語言的特性,這裏能夠經過HTTP的方法列表動態生成函數內容。

想要動態生成函數,首先須要肯定函數名稱。函數名就是nodejs中HTTP服務器支持的方法名稱,能夠在官方文檔中獲取,具體參數是http.METHODS。這個屬性是在v0.11.8新增的,若是nodejs低於該版本,須要手動創建一個方法列表,具體能夠參考nodejs代碼。

express框架HTTP方法名的獲取封裝到另外一個包,叫作methods,內部給出了低版本的兼容動詞列表。

function getBasicNodeMethods() {
  return [
    'get',
    'post',
    'put',
    'head',
    'delete',
    'options',
    'trace',
    'copy',
    'lock',
    'mkcol',
    'move',
    'purge',
    'propfind',
    'proppatch',
    'unlock',
    'report',
    'mkactivity',
    'checkout',
    'merge',
    'm-search',
    'notify',
    'subscribe',
    'unsubscribe',
    'patch',
    'search',
    'connect'
  ];
}複製代碼

知道所支持的方法名列表數組後,剩下的只須要一個for循環生成全部的函數便可。

全部的動詞處理函數的核心內容都在Route中。

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Route.prototype[method] = function(fn) {
        var layer = new Layer('/', fn);
        layer.method = method;

        this.methods[method] = true;
        this.stack.push(layer);

        return this;
    };
});複製代碼

接着修改Router。

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Router.prototype[method] = function(path, fn) {
        var route = this.route(path);
        route[method].call(route, fn);

        return this;
    };
});複製代碼

最後修改application.js的內容。這裏改動較大,從新定義了一個Application類,而不是使用字面量直接建立。

function Application() {
    this._router = new Router();
}


Application.prototype.listen = function(port, cb) {
    var self = this;

    var server = http.createServer(function(req, res) {
        self.handle(req, res);
    });

    return server.listen.apply(server, arguments);
};


Application.prototype.handle = function(req, res) {
    if(!res.send) {
        res.send = function(body) {
            res.writeHead(200, {
                'Content-Type': 'text/plain'
            });
            res.end(body);
        };
    }

    var router = this._router;
    router.handle(req, res);
};複製代碼

接着增長HTTP方法函數。

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Application.prototype[method] = function(path, fn) {
        this._router[method].apply(this._router, arguments);
        return this;
    };
});複製代碼

由於導出的是Application類,因此修改expross.js文件。

var Application = require('./application');

function createApplication() {
    var app = new Application();
    return app;
}複製代碼

運行node test/index.js,走起。

若是你仔細研究路由系統的源碼,會發現route自己的定義並非像文字描述那樣。例如:

增長兩個一樣路徑的路由:

app.put('/', function(req, res) {
    res.send('put Hello World!');
});

app.get('/', function(req, res) {
    res.send('get Hello World!');
});複製代碼

結果並非想象中相似下面的結構:

---------------------------------------------------------
 ----- -----------        |     0     |     1     |     2     |     3     |  ...  |
|     | Layer     |       ---------------------------------------------------------
|  0  |   |-path  |       | Layer     | Layer     | Layer     | Layer     |       |
|     |   |-route | ----> |  |- method|  |- method|  |- method|  |- method|  ...  |
|-----|-----------|       |  |- handle|  |- handle|  |- handle|  |- handle|       |
|     | Layer     |       ---------------------------------------------------------
|  1  |   |-path  |                                  route
|     |   |-route |       
|-----|-----------|       
|     | Layer     |
|  2  |   |-path  |
|     |   |-route |
|-----|-----------|
| ... |   ...     |
 ----- ----------- 
      router複製代碼

而是以下的結構:

----- -----------        -------------
|     | Layer     | ----> | Layer     |
|  0  |   |-path  |       |  |- method|   route
|     |   |-route |       |  |- handle|
|-----|-----------|       -------------
|     | Layer     |       -------------      
|  1  |   |-path  | ----> | Layer     |
|     |   |-route |       |  |- method|   route     
|-----|-----------|       |  |- handle|        
|     | Layer     |       -------------
|  2  |   |-path  |       -------------  
|     |   |-route | ----> | Layer     |
|-----|-----------|       |  |- method|   route
| ... |   ...     |       |  |- handle| 
 ----- -----------        -------------
    router複製代碼

之因此會這樣是由於路由系統存在這前後順序的關係,若是你前面的描述結構頗有可能會丟失路由順序這個屬性。既然這樣,那Route的用處是在哪?

由於在express框架中,Route存儲的是真正的路由信息,能夠當作單獨的成員使用,若是想要真正前面的所描述的結果描述,你須要這樣建立你的代碼:

var router = express.Router();

router.route('/users/:user_id')
.get(function(req, res, next) {
  res.json(req.user);
})
.put(function(req, res, next) {
  // just an example of maybe updating the user
  req.user.name = req.params.name;
  // save user ... etc
  res.json(req.user);
})
.post(function(req, res, next) {
  next(new Error('not implemented'));
})
.delete(function(req, res, next) {
  next(new Error('not implemented'));
});複製代碼

而不是這樣:

var app = expross();

app.get('/users/:user_id', function(req, res) {

})

.put('/users/:user_id', function(req, res) {

})

.post('/users/:user_id', function(req, res) {

})

.delete('/users/:user_id', function(req, res) {

});複製代碼

理解了Route的使用方法,接下來就要討論剛剛提到的順序問題。在路由系統中,路由的處理順序很是重要,由於路由是按照數組的方式存儲的,若是碰見兩個一樣的路由,一樣的方法名,不一樣的處理函數,這時候先後聲明的順序將直接影響結果(這也是express中間件存在順序相關的緣由),例以下面的例子:

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('first');
});

app.get('/', function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('second');
});複製代碼

上面的代碼若是執行會發現永遠都返回first,可是有的時候會根據前臺傳來的參數動態判斷是否執行接下來的路由,怎樣才能跳過first進入second?這就涉及到路由系統的流程控制問題。

流程控制分爲主動和被動兩種模式。

對於expross框架來講,路由綁定的處理邏輯、用戶設置的路徑參數這些都是不可靠的,在運行過程當中頗有可能會發生異常,被動流程控制就是當這些異常發生的時候,expross框架要擔負起捕獲這些異常的工做,由於若是不明確異常的發生位置,會致使js代碼沒法繼續運行,而且沒法準確的報出故障。

主動流程控制則是處理函數內部的操做邏輯,以主動調用的方式來跳轉路由內部的執行邏輯。

目前express經過引入next參數的方式來解決流程控制問題。next是處理函數的一個參數,其自己也是一個函數,該函數有幾種使用方式:

  • 執行下一個處理函數。執行next()
  • 報告異常。執行next(err)
  • 跳過當前Route,執行Router的下一項。執行next('route')
  • 跳過整個Router。執行next('router')

接下來,咱們嘗試實現如下這些需求。

首先修改最底層的Layer對象,該對象的handle_request函數是負責調用路由綁定的處理邏輯,這裏添加next參數,而且增長異常捕獲功能。

Layer.prototype.handle_request = function (req, res, next) {
  var fn = this.handle;

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};複製代碼

接下來修改Route.dispath函數。由於涉及到內部的邏輯跳轉,使用for循環循序漸進不太合適,這裏使用了相似遞歸的方式。

Route.prototype.dispatch = function(req, res, done) {
    var self = this,
        method = req.method.toLowerCase(),
        idx = 0, stack = self.stack;

    function next(err) {
        //跳過route
        if(err && err === 'route') {
            return done();
        }

        //跳過整個路由系統
        if(err && err === 'router') {
            return done(err);
        }

        //越界
        if(idx >= stack.length) {
            return done(err);
        }

        //不等枚舉下一個
        var layer = stack[idx++];
        if(method !== layer.method) {
            return next(err);
        }

        if(err) {
            //主動報錯
            return done(err);
        } else {
            layer.handle_request(req, res, next);
        }
    }

    next();
};複製代碼

整個處理過程本質上仍是一個for循環,惟一的差異就是在處理函數中用戶主動調用next函數的處理邏輯。

若是用戶經過next函數返回錯誤、routerouter這三種狀況,目前統一拋給Router處理。

由於修改了dispatch函數,因此調用該函數的Router.route函數也要修改,這回直接改完全,之後無需根據參數的個數進行調整。

Router.prototype.route = function route(path) {
    ...

    //使用bind方式
    var layer = new Layer(path, route.dispatch.bind(route));

    ...
};複製代碼

接着修改Router.handle的代碼,邏輯和Route.dispatch相似。

Router.prototype.handle = function(req, res, done) {
    var self = this,
        method = req.method,
        idx = 0, stack = self.stack;

    function next(err) {
        var layerError = (err === 'route' ? null : err);

        //跳過路由系統
        if(layerError === 'router') {
            return done(null);
        }

        if(idx >= stack.length || layerError) {
            return done(layerError);
        }

        var layer = stack[idx++];
        //匹配,執行
        if(layer.match(req.url) && layer.route &&
            layer.route._handles_method(method)) {
            return layer.handle_request(req, res, next);
        } else {
            next(layerError);
        }
    }

    next();
};複製代碼

修改後的函數處理過程和原來的相似,不過有一點須要注意,當發生異常的時候,會將結果返回給上一層,而不是執行原有this.stack第0層的代碼邏輯。

最後增長expross框架異常處理的邏輯。

在以前,能夠移除原有this.stack的初始化代碼,將

var Router = function() {
    this.stack = [new Layer('*', function(req, res) {
        res.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        res.end('404');        
    })];
};複製代碼

改成

var Router = function() {
    this.stack = [];
};複製代碼

而後,修改Application.handle函數。

Application.prototype.handle = function(req, res) {

    ...

    var done = function finalhandler(err) {
        res.writeHead(404, {
            'Content-Type': 'text/plain'
        });

        if(err) {
            res.end('404: ' + err);    
        } else {
            var msg = 'Cannot ' + req.method + ' ' + req.url;
            res.end(msg);    
        }
    };

    var router = this._router;
    router.handle(req, res, done);
};複製代碼

這裏簡單的將done函數處理爲返回404頁面,其實在express框架中,使用的是一個單獨的npm包,叫作finalhandler

簡單的修改一下測試用例證實一下成果。

var expross = require('../');
var app = expross();

app.get('/', function(req, res, next) {
    next();
})

.get('/', function(req, res, next) {
    next(new Error('error'));
})

.get('/', function(req, res) {
    res.send('third');
});

app.listen(3000, function() {
    console.log('Example app listening on port 3000!');
});複製代碼

運行node test/index.js,訪問http://127.0.0.1:3000/,結果顯示:

404: Error: error複製代碼

貌似目前一切都很順利,不過還有一個需求目前被忽略了。當前處理函數的異常所有是由框架捕獲,返回的信息只能是固定的404頁面,對於框架使用者顯然很不方便,大多數時候,咱們都但願能夠捕獲錯誤,並按照必定的信息封裝返回給瀏覽器,因此expross須要一個返回錯誤給上層使用者的接口。

目前和上層對接的處理函數的聲明以下:

function process_fun(req, res, next) {

}複製代碼

若是增長一個錯誤處理函數,按照nodejs的規則,第一個參數是錯誤信息,定義應該以下所示:

function process_err(err, req, res, next) {

}複製代碼

由於兩個聲明的第一個參數信息是不一樣的,若是區分傳入的處理函數是處理錯誤的函數仍是處理正常的函數,這個是expross框架須要搞定的關鍵問題。

javascript中,Function.length屬性能夠獲取傳入函數指定的參數個數,這個能夠當作區分兩者的關鍵信息。

既然肯定了原理,接下來直接在Layer類上增長一個專門處理錯誤的函數,和處理正常信息的函數區分開。

//錯誤處理
Layer.prototype.handle_error = function (error, req, res, next) {
  var fn = this.handle;

  //若是函數參數不是標準的4個參數,返回錯誤信息
  if(fn.length !== 4) {
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};複製代碼

接着修改Route.dispatch函數。

Route.prototype.dispatch = function(req, res, done) {
    var self = this,
        method = req.method.toLowerCase(),
        idx = 0, stack = self.stack;

    function next(err) {

        ...

        if(err) {
            //主動報錯
            layer.handle_error(err, req, res, next);
        } else {
            layer.handle_request(req, res, next);
        }
    }

    next();
};複製代碼

當發生錯誤的時候,Route會一直向後尋找錯誤處理函數,若是找到則返回,不然執行done(err),將錯誤拋給Router。

對於Router.handle的修改,由於涉及到一些中間件的概念,完整的錯誤處理將推移到下一節完成。

本節的內容基本上完成,包括HTTP相關的動做接口的添加、路由系統的流程跳轉以及Route級別的錯誤響應等等,涉及到路由系統剩下的一點內容暫時放到之後講解。

4. 第四次迭代

本節是expross的第四次迭代,主要的目標是創建中間件機制並繼續完善路由系統的功能。

在express中,中間件實際上是一個介於web請求來臨後到調用處理函數前整個流程體系中間調用的組件。其本質是一個函數,內部能夠訪問修改請求和響應對象,並調整接下來的處理流程。

express官方給出的解釋以下:

Express 是一個自身功能極簡,徹底是由路由和中間件構成一個的 web 開發框架:從本質上來講,一個 Express 應用就是在調用各類中間件。

中間件(Middleware) 是一個函數,它能夠訪問請求對象(request object (req)), 響應對象(response object (res)), 和 web 應用中處於請求-響應循環流程中的中間件,通常被命名爲 next 的變量。

中間件的功能包括:

  • 執行任何代碼。
  • 修改請求和響應對象。
  • 終結請求-響應循環。
  • 調用堆棧中的下一個中間件。

若是當前中間件沒有終結請求-響應循環,則必須調用 next() 方法將控制權交給下一個中間件,不然請求就會掛起。

Express 應用可以使用以下幾種中間件:

使用可選則掛載路徑,可在應用級別或路由級別裝載中間件。另外,你還能夠同時裝在一系列中間件函數,從而在一個掛載點上建立一個子中間件棧。

官方給出的定義其實已經足夠清晰,一箇中間件的樣式其實就是上一節所提到的處理函數,只不過並無正式命名。因此對於代碼來講Router類中的this.stack屬性內部的每一個handle都是一箇中間件,根據使用接口不一樣區別了應用級中間件路由級中間件,而四個參數的處理函數就是錯誤處理中間件

接下來就給expross框架添加中間件的功能。

首先是應用級中間件,其使用方法是Application類上的兩種方式:Application.use 和 Application.METHOD (HTTP的各類請求方法),後者其實在前面的小節裏已經實現了,前者則是須要新增的。

在前面的小節裏的代碼已經說明Application.METHOD內部實際上是Router.METHOD的代理,Application.use一樣如此。

Application.prototype.use = function(fn) {
    var path = '/',
        router = this._router;

    router.use(path, fn);

    return this;
};複製代碼

由於Application.use支持可選路徑,因此須要增長處理路徑的重載代碼。

Application.prototype.use = function(fn) {
    var path = '/',
        router = this._router;

    //路徑掛載
    if(typeof fn !== 'function') {
        path = fn;
        fn = arguments[1];
    }

    router.use(path, fn);

    return this;
};複製代碼

其實express框架支持的參數不只僅這兩種,可是爲了便於理解剔除了一些旁枝末節,便於框架的理解。

接下來實現Router.use函數。

Router.prototype.use = function(fn) {
    var path = '/';

    //路徑掛載
    if(typeof fn !== 'function') {
        path = fn;
        fn = arguments[1];
    }

    var layer = new Layer(path, fn);
    layer.route = undefined;

    this.stack.push(layer);

    return this;
};複製代碼

內部代碼和Application.use差很少,只不過最後再也不是調用Router.use,而是直接建立一個Layer對象,將其放到this.stack數組中。

在這裏段代碼裏能夠看到普通路由和中間件的區別。普通路由放到Route中,且Router.route屬性指向Route對象,Router.handle屬性指向Route.dispatch函數;中間件的Router.route屬性爲undefined,Router.handle指向中間件處理函數,被放到Router.stack數組中。

對於路由級中間件,首先按照要求導出Router類,便於使用。

exports.Router = Router;複製代碼

上面的代碼添加到expross.js文件中,這樣就能夠按照下面的使用方式建立一個單獨的路由系統。

var app = express();
var router = express.Router();

router.use(function (req, res, next) {
  console.log('Time:', Date.now());
});複製代碼

如今問題來了,若是像上面的代碼同樣建立一個新的路由系統是沒法讓路由系統內部的邏輯生效的,由於這個路由系統無法添加到現有的系統中。

一種辦法是增長一個專門添加新路由系統的接口,是徹底是可行的,可是我更欣賞express框架的辦法,這多是Router叫作路由級中間件的緣由。express將Router定義成一個特殊的中間件,而不是一個單獨的類。

這樣將單首創建的路由系統添加到現有的應用中的代碼很是簡單通用:

var router = express.Router();

// 將路由掛載至應用
app.use('/', router);複製代碼

這確實是一個好方法,如今就來將expross修改爲相似的樣子。

首先建立一個原型對象,將現有的Router方法轉移到該對象上。

var proto = {};

proto.handle = function(req, res, done) {...};
proto.route = function route(path) {...};
proto.use = function(fn) { ... };

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    proto[method] = function(path, fn) {
        var route = this.route(path);
        route[method].call(route, fn);

        return this;
    };
});複製代碼

而後建立一箇中間件函數,使用Object.setPrototypeOf函數設置其原型,最後導出一個生成這些過程的函數。

module.exports = function() {
    function router(req, res, next) {
        router.handle(req, res, next);
    }

    Object.setPrototypeOf(router, proto);

    router.stack = [];
    return router;
};複製代碼

修改測試用例,測試一下效果。

app.use(function(req, res, next) {
    console.log('Time:', Date.now());
    next();
});

app.get('/', function(req, res, next) {
    res.send('first');
});


router.use(function(req, res, next) {
    console.log('Time: ', Date.now());
    next();
});

router.use('/', function(req, res, next) {
    res.send('second');
});

app.use('/user', router);

app.listen(3000, function() {
    console.log('Example app listening on port 3000!');
});複製代碼

結果並不理想,原有的應用程序還有兩個地方須要修改。

首先是邏輯處理問題。原有的Router.handle函數中並無處理中間件的狀況,須要進一步修改。

proto.handle = function(req, res, done) {

    ...

    function next(err) {

        ...

        //注意這裏,layer.route屬性
        if(layer.match(req.url) && layer.route &&
            layer.route._handles_method(method)) {
            layer.handle_request(req, res, next);
        } else {
            layer.handle_error(layerError, req, res, next);
        }
    }

    next();
};複製代碼

改爲:

proto.handle = function(req, res, done) {

    ...

    function next(err) {

        ...

        //匹配,執行
        if(layer.match(path)) {
            if(!layer.route) {
                //處理中間件
                layer.handle_request(req, res, next);    
            } else if(layer.route._handles_method(method)) {
                //處理路由
                layer.handle_request(req, res, next);
            }    
        } else {
            layer.handle_error(layerError, req, res, next);
        }
    }

    next();
};複製代碼

其次是路徑匹配的問題。原有的單一路徑被拆分紅爲不一樣中間的路徑組合,這裏判斷須要多步進行,由於每一箇中間件只是匹配本身的路徑是否經過,不過相對而言目前涉及的匹配都是全等匹配,尚未涉及到相似express框架中的正則匹配,算是很是簡單了。

想要實現匹配邏輯就要清楚的知道哪段路徑和哪一個處理函數匹配,這裏定義三個變量:

  • req.originalUrl 原始請求路徑。
  • req.url 當前路徑。
  • req.baseUrl 父路徑。

主要修改Router.handle函數,該函數主要負責提取當前路徑段,便於和事先傳入的路徑進行匹配。

proto.handle = function(req, res, done) {
    var self = this,
        method = req.method,
        idx = 0, stack = self.stack,
        removed = '', slashAdded = false;


    //獲取當前父路徑
    var parentUrl = req.baseUrl || '';
    //保存父路徑
    req.baseUrl = parentUrl;
    //保存原始路徑
    req.orginalUrl = req.orginalUrl || req.url;


    function next(err) {
        var layerError = (err === 'route' ? null : err);

        //若是有移除,復原原有路徑
        if(slashAdded) {
            req.url = '';
            slashAdded = false;
        }


        //若是有移除,復原原有路徑信息
        if(removed.length !== 0) {
            req.baseUrl = parentUrl;
            req.url = removed + req.url;
            removed = '';
        }


        //跳過路由系統
        if(layerError === 'router') {
            return done(null);
        }


        if(idx >= stack.length || layerError) {
            return done(layerError);
        }

        //獲取當前路徑
        var path = require('url').parse(req.url).pathname;

        var layer = stack[idx++];
        //匹配,執行
        if(layer.match(path)) {

            //處理中間件
            if(!layer.route) {
                //要移除的部分路徑
                removed = layer.path;

                //設置當前路徑
                req.url = req.url.substr(removed.length);
                if(req.url === '') {
                    req.url = '/' + req.url;
                    slashAdded = true;
                }

                //設置當前路徑的父路徑
                req.baseUrl = parentUrl + removed;

                //調用處理函數
                layer.handle_request(req, res, next);    
            } else if(layer.route._handles_method(method)) {
                //處理路由
                layer.handle_request(req, res, next);
            }    
        } else {
            layer.handle_error(layerError, req, res, next);
        }
    }

    next();
};複製代碼

這段代碼主要處理兩種狀況:

第一種,存在路由中間件的狀況。如:

router.use('/1', function(req, res, next) {
    res.send('first user');
});

router.use('/2', function(req, res, next) {
    res.send('second user');
});

app.use('/users', router);複製代碼

這種狀況下,Router.handle順序匹配到中間的時候,會遞歸調用Router.handle,因此須要保存當前的路徑快照,具體路徑相關信息放到req.url、req.originalUrl 和req.baseUrl 這三個參數中。

第二種,非路由中間件的狀況。如:

app.get('/', function(req, res, next) {
    res.send('home');
});

app.get('/books', function(req, res, next) {
    res.send('books');
});複製代碼

這種狀況下,Router.handle內部主要是按照棧中的次序匹配路徑便可。

改好了處理函數,還須要修改一下Layer.match這個匹配函數。目前建立Layer可能會有三種狀況:

  • 不含有路徑的中間件。path屬性默認爲/
  • 含有路徑的中間件。
  • 普通路由。若是path屬性爲*,表示任意路徑。

修改原有Layer是構造函數,增長一個fast_star 標記用來判斷path是否爲*。

function Layer(path, fn) {
  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.path = undefined;

  //是否爲*
  this.fast_star = (path === '*' ? true : false);
  if(!this.fast_star) {
    this.path = path;
  }
}複製代碼

接着修改Layer.match匹配函數。

Layer.prototype.match = function(path) {

  //若是爲*,匹配
  if(this.fast_star) {
    this.path = '';
    return true;
  }

  //若是是普通路由,從後匹配
  if(this.route && this.path === path.slice(-this.path.length)) {
    return true;
  }

  if (!this.route) {
    //不帶路徑的中間件
    if (this.path === '/') {
      this.path = '';
      return true;
    }

    //帶路徑中間件
    if(this.path === path.slice(0, this.path.length)) {
      return true;
    }
  }

  return false;
};複製代碼

代碼中一共判斷四種狀況,根據this.route區分中間件和普通路由,而後分開判斷。

express除了普通的中間件外還要一種錯誤中間件,專門用來處理錯誤信息。該中間件的聲明和上一小節最後介紹的錯誤處理函數是同樣的,一樣是四個參數分別是:err、 req、 res和 next。

目前Router.handle中,當碰見錯誤信息的時候,會直接經過回調函數返回錯誤信息,顯示錯誤頁面。

if(idx >= stack.length || layerError) {
    return done(layerError);
}複製代碼

這裏須要修改策略,將其改成繼續調用下一個中間件,直到碰到錯誤中間件爲止。

//沒有找到
if(idx >= stack.length) {
    return done(layerError);
}複製代碼

原有這一塊的代碼只保留判斷枚舉是否完成,將錯誤判斷轉移到最後執行處理函數的位置。之因此這樣作是由於無論是執行處理函數,或是執行錯誤處理都須要進行路徑匹配操做和路徑分析操做,因此放到後面正好合適。

//處理中間件
if(!layer.route) {

    ...

    //調用處理函數
    if(layerError)
        layer.handle_error(layerError, req, res, next);
    else
        layer.handle_request(req, res, next);

} else if(layer.route._handles_method(method)) {
    //處理路由
    layer.handle_request(req, res, next);
}複製代碼

到此爲止,expross的錯誤處理部分算是基本完成了。

路由系統和中間件兩個大的概念算是所有講解完畢,固然還有不少細節沒有完善,在剩下的文字裏若是有必要會繼續完善。

下一節主要的內容是介紹先後端交互的兩個重要成員:request和response。express在nodejs的基礎之上進行了豐富的擴展,因此頗有必要仿製一下。

5. 第五次迭代

本節是expross的第五次迭代,主要的目標是封裝request和response兩個對象,方便使用。

其實nodejs已經給咱們提供這兩個默認的對象,之因此要封裝是由於豐富一下兩者的接口,方便框架使用者,目前框架在response對象上已經有一個接口:

if(!res.send) {
    res.send = function(body) {
        res.writeHead(200, {
            'Content-Type': 'text/plain'
        });
        res.end(body);
    };
}複製代碼

若是須要繼續封裝,也要相似的結構在代碼上添加顯然會給人一種很亂的感受,由於request和response的原始版本是nodejs提供給框架的,框架獲取到的是兩個對象,並非類,要想在兩者之上提供另外一組接口的辦法有不少,歸根結底就是將新的接口加到該對象上或者加到該對象的原型鏈上,目前的代碼選擇了前者,express的代碼選擇了後者。

首先創建兩個文件:request.js 和 response.js,兩者分別導出req和res對象。

//request.js
var http = require('http');

var req = Object.create(http.IncomingMessage.prototype);

module.exports = req;


//response.js
var http = require('http');

var res = Object.create(http.ServerResponse.prototype);

module.exports = res;複製代碼

兩者文件的代碼都是建立一個對象,分別指向nodejs提供的request和response兩個對象的原型,之後expross自定的接口能夠統一掛載到這兩個對象上。

接着修改Application.handle函數,由於這個函數裏面有新鮮出爐的request和response。思路很簡單,就是將兩者的原型指向咱們自建的req和res。由於req和res對象的原型和request和response的原型相同,因此並不影響原有nodejs的接口。

var request = require('./request');
var response = require('./response');

...

Application.prototype.handle = function(req, res) {

    Object.setPrototypeOf(req, request);
    Object.setPrototypeOf(res, response);


    ...
};複製代碼

這裏將原有的res.send轉移到了response.js文件中。

res.send = function(body) {
    this.writeHead(200, {
        'Content-Type': 'text/plain'
    });
    this.end(body);
};複製代碼

注意函數中不在是res.writeHead和res.end,而是this.writeHead和this.end。

在整個路由系統中,Router.stack每一項其實都是一箇中間件,每一箇中間件都有可能用到req和res這兩個對象,因此代碼中修改nodejs原生提供的request和response對象的代碼放到了Application.handle中,這樣作並無問題,可是有一種更好的方式,expross框架將這部分代碼封裝成了一個內部中間件。

爲了確保框架中每一箇中間件接收這兩個參數的正確性,須要將該內部中間放到Router.stack的首項。這裏將原有Application的構造函數中的代碼去掉,再也不是直接建立Router()路由系統,而是用一種惰性加載的方式,動態建立。

去除原有Application構造函數的代碼。

function Application() {}複製代碼

添加惰性初始化函數。

Application.prototype.lazyrouter = function() {
    if(!this._router) {
        this._router = new Router();

        this._router.use(middleware.init());
    }
};複製代碼

由於是惰性初始化,因此在使用this._router對象前,必定要先調用該函數動態建立this._router對象。相似以下代碼:

//獲取router
this.lazyrouter();
router = this._router;複製代碼

接下來建立一個叫middleware文件夾,專門放內部中間件的文件,再建立一個init.js文件,放置Application.handle中用來初始化res和req的代碼。

var request = require('../request');
var response = require('../response');

exports.init = function expressInit(req, res, next) {
    //request文件可能用到res對象
    req.res = res;

    //response文件可能用到req對象
    res.req = req;

    //修改原始req和res原型
    Object.setPrototypeOf(req, request);
    Object.setPrototypeOf(res, response);

    //繼續
    next();
};複製代碼

修改原有的Applicaton.handle函數。

Application.prototype.handle = function(req, res) {

    ...

    // 這裏無需調用lazyrouter,由於listen前必定調用了.use或者.METHODS方法。
    // 若是兩者都沒有調用,沒有必要建立路由系統。this._router爲undefined。
    var router = this._router;
    if(router) {
        router.handle(req, res, done);
    } else {
        done();
    }
};複製代碼

運行node test/index.js走起……

express框架中,request和response兩個對象有不少很是好用的函數,不過大部分和框架結構無關,而且這些函數內部過於專研細節,對框架自己的理解沒有多少幫助。不過接下來有一個方面須要仔細研究一下,那就是先後端參數的傳遞,express如何獲取並分類這些參數的,這一點仍是須要略微瞭解。

默認狀況下,一共有三種參數獲取方式。

  • req.query 表明查詢字符串。
  • req.params 表明路徑變量。
  • req.body 表明表單提交變量。

req.query 是最經常使用的方式,例如:

// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"

// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"

req.query.shoe.color
// => "blue"

req.query.shoe.type
// => "converse"複製代碼

後臺獲取這些參數最簡單的方式就是經過nodejs自帶的querystring模塊分析URL。express使用的是另外一個npm包,qs。而且將其封裝爲另外一個內部中間件,專門負責解析查詢字符串,默認加載。

req.params 是另外一種從URL獲取參數的方式,例如:

//路由規則  /user/:name
// GET /user/tj
req.params.name
// => "tj"複製代碼

這是一種express框架規定的參數獲取方式,對於批量處理邏輯很是實用。在expross中並無實現,由於路徑匹配問題過於細節化,若是對此感興趣能夠研究研究path-to-regexp模塊,express也是在其上的封裝。

req.body 是獲取表單數據的方式,可是默認狀況下框架是不會去解析這種數據,直接使用只會返回undefined。若是想要支持須要添加其餘中間件,例如 body-parsermulter

本小節主要介紹了request和response兩個對象,而且講解了一下現有expross框架中獲取參數的方式,總體上並無太深刻的仿製,主要是這方面內容涉及的細節過多,只可意會。研究了也就僅僅知道而已,並不能帶來多少積累,除非重頭再造一次輪子。

6. 第六次迭代

本小節是第六次迭代,主要的目的是介紹一下expresss是如何集成現有的渲染引擎的。與渲染引擎有關的事情涉及到下面幾個方面:

  • 如何開發或綁定一個渲染引擎。
  • 如何註冊一個渲染引擎。
  • 如何指定模板路徑。
  • 如何渲染模板引擎。

express經過app.engine(ext, callback) 方法便可建立一個你本身的模板引擎。其中,ext 指的是文件擴展名、callback 是模板引擎的主函數,接受文件路徑、參數對象和回調函數做爲其參數。

//下面的代碼演示的是一個很是簡單的可以渲染 「.ntl」 文件的模板引擎。

var fs = require('fs'); // 此模板引擎依賴 fs 模塊
app.engine('ntl', function (filePath, options, callback) { // 定義模板引擎
  fs.readFile(filePath, function (err, content) {
    if (err) return callback(new Error(err));
    // 這是一個功能極其簡單的模板引擎
    var rendered = content.toString().replace('#title#', '<title>'+ options.title +'</title>')
    .replace('#message#', '<h1>'+ options.message +'</h1>');
    return callback(null, rendered);
  })
});複製代碼

爲了讓應用程序能夠渲染模板文件,還須要作以下設置:

//views, 放模板文件的目錄
app.set('views', './views')
//view engine, 模板引擎
app.set('view engine', 'jade')複製代碼

一旦 view engine 設置成功,就不須要顯式指定引擎,或者在應用中加載模板引擎模塊,Express 已經在內部加載。下面是如何渲染頁面的方法:

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!'});
});複製代碼

要想實現上述功能,首先在Application類中定義兩個變量,一個存儲app.set 和 app.get 這兩個方法存儲的值,另外一個存儲模板引擎中擴展名和渲染函數的對應關係。

function Application() {
    this.settings = {};
    this.engines = {};
}複製代碼

而後是實現app.set函數。

Application.prototype.set = function(setting, val) {
      if (arguments.length === 1) {
      // app.get(setting)
      return this.settings[setting];
    }

    this.settings[setting] = val;
    return this;
};複製代碼

代碼中不只僅實現了設置,如何傳入的參數只有一個等價於get函數。

接着實現app.get函數。由於如今已經有了一個app.get方法用來設置路由,因此須要在該方法上進行重載。

http.METHODS.forEach(function(method) {
    method = method.toLowerCase();
    Application.prototype[method] = function(path, fn) {
        if(method === 'get' && arguments.length === 1) {
            // app.get(setting)
            return this.set(path);
        }

        ...
    };
});複製代碼

最後實現app.engine進行擴展名和引擎函數的映射。

Application.prototype.engine = function(ext, fn) {
    // get file extension
    var extension = ext[0] !== '.'
      ? '.' + ext
      : ext;

    // store engine
    this.engines[extension] = fn;

    return this;
};複製代碼

擴展名當作key,統一添加「.」。

到此設置模板引擎相關信息的函數算是完成,接下來就是最重要的渲染引擎函數的實現。

res.render = function(view, options, callback) {
      var app = this.req.app;
    var done = callback;
    var opts = options || {};
    var self = this;

    //若是定義回調,則返回,不然渲染
    done = done || function(err, str) {
        if(err) {
            return req.next(err);
        }

        self.send(str);
    };

    //渲染
    app.render(view, opts, done);
};複製代碼

渲染函數一共有三個參數,view表示模板的名稱,options是模板渲染的變量,callback是渲染成功後的回調函數。

函數內部直接調用render函數進行渲染,渲染完成後調用done回調。

接下來建立一個view.js文件,主要功能是負責各類模板引擎和框架間的隔離,保持對內接口的統一性。

function View(name, options) {
    var opts = options || {};

    this.defaultEngine = opts.defaultEngine;
    this.root = opts.root;

    this.ext = path.extname(name);
    this.name = name;


    var fileName = name;
    if (!this.ext) {
      // get extension from default engine name
      this.ext = this.defaultEngine[0] !== '.'
        ? '.' + this.defaultEngine
        : this.defaultEngine;

      fileName += this.ext;
    }


    // store loaded engine
    this.engine = opts.engines[this.ext];

    // lookup path
    this.path = this.lookup(fileName);
}複製代碼

View類內部定義了不少屬性,主要包括引擎、根目錄、擴展名、文件名等等,爲了之後的渲染作準備。

View.prototype.render = function render(options, callback) {
    this.engine(this.path, options, callback);
};複製代碼

View的渲染函數內部就是調用一開始註冊的引擎渲染函數。

瞭解了View的定義,接下來實現app.render模板渲染函數。

Application.prototype.render = function(name, options, callback) {
    var done = callback;
    var engines = this.engines;
    var opts = options;

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });


    if (!view.path) {
      var err = new Error('Failed to lookup view "' + name + '"');
      return done(err);
    }


    try {
      view.render(options, callback);
    } catch (e) {
      callback(e);
    }
};複製代碼

還有一些細節沒有在教程中展現出來,能夠參考github上傳的案例代碼。

貌似一切搞定,修改測試用例,嘗試一下。

var fs = require('fs'); // 此模板引擎依賴 fs 模塊
app.engine('ntl', function (filePath, options, callback) { // 定義模板引擎
  fs.readFile(filePath, function (err, content) {
    if (err) return callback(new Error(err));
    // 這是一個功能極其簡單的模板引擎
    var rendered = content.toString().replace('#title#', '<title>'+ options.title +'</title>')
    .replace('#message#', '<h1>'+ options.message +'</h1>');
    return callback(null, rendered);
  });
});

app.set('views', './test/views'); // 指定視圖所在的位置
app.set('view engine', 'ntl'); // 註冊模板引擎


app.get('/', function(req, res, next) {
    res.render('index', { title: 'Hey', message: 'Hello there!'});
});複製代碼

運行node test/index.js,查看效果。

上面的代碼是本身註冊的引擎,若是想要和現有的模板引擎結合還須要在回調函數中引用模板自身的渲染方法,固然爲了方便,express框架內部提供了一個默認方法,若是模板引擎導出了該方法,則表示該模板引擎支持express框架,無需使用app.engine再次封裝。

該方法聲明以下:

__express(filePath, options, callback)複製代碼

能夠參考ejs模板引擎的代碼,看看它們是如何寫的:

//該行代碼在lib/ejs.js文件的355行左右
exports.__express = exports.renderFile;複製代碼

express框架是如何實現這個默認加載的功能的呢?很簡單,只須要在View的構造函數中加一個判斷便可。

if (!opts.engines[this.ext]) {
  // load engine
  var mod = this.ext.substr(1);
  opts.engines[this.ext] = require(mod).__express;
}複製代碼

代碼很簡單,若是沒有找到引擎對應的渲染函數,那就嘗試加載__express函數。

後記

至此,算是結束本篇文章了。其實還有不少內容能夠寫,可是寫的有些煩了,篇幅太長了,大概有一萬三千多字,後面有點應付了事的感受。

簡單的說一下還有哪裏沒有介紹。

  1. 關於Application。

若是稍微看過expross代碼的人都會發現,Application並非想我寫的這樣是一個類,而是一箇中間件,一個對象,該對象使用了mixin方法的多繼承手段,express.js文件中的createApplication函數算是整個框架的切入點。

  1. 關於Router.handle。

這個函數能夠說是整個express框架的核心,若是理解了該函數,整個框架基本上就掌握了。我在仿製的時候捨棄了不少細節,在這裏個函數裏面內部有兩個關鍵點沒說。1、處理URL形式的參數,這裏涉及對params參數的提取過程。其中有一個restore函數使用高階函數的方法作了緩存,仔細體會頗有意思。2、setImmediate異步返回,之因此要使用異步處理,是由於下面的代碼須要運行,包括路徑相關的參數,這些參數在下一個處理函數中可能會用到。

  1. 關於其餘函數。

太多函數了,不一一列舉,前文已經提到,涉及的細節太多,正則表達式,http協議層,nodejs自己函數的使用,對於整個框架的理解幫助不大,所有捨棄。不過大多數函數都是自成體系,很好理解。

相關文章
相關標籤/搜索