express源碼解讀

引用:https://github.com/WangZhechao/expross

1.簡介

這篇文章是分析express框架的源碼辩稽,網(wǎng)上關(guān)于源碼的分析已經(jīng)很多了似芝,這篇文章通過仿制一個express框架龙誊,以測試驅(qū)動的方式不斷迭代,正向理解express源碼。
這篇文章中express源碼的版本為(v4.15.4),文章的整體思路參考早期的另一篇文章(https://segmentfault.com/a/1190000005833119)捺宗。
準備學(xué)習(xí)express源碼的基本原理,前提條件式有一定的express框架使用經(jīng)驗川蒙,來掌握其背后的原理蚜厉。

2.框架初始化

在仿制express框架前,首先完成兩件事:

  • 確認需求
  • 確認結(jié)構(gòu)
    首先確認需求畜眨,從express官網(wǎng)入手昼牛,網(wǎng)站有一個Hello world的示例程序,想要仿制express康聂,該程序肯定要通過測試贰健,將該代碼負值保存到測試目錄test/index.js。
const express = require('express');
const app = express();

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

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

接下來恬汁,確認框架名稱及目錄伶椿,框架名稱為express,目錄結(jié)構(gòu)如下:

express
  |
  |-- lib
  |    | 
  |    |-- express.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js

然express/index.js文件加載lib目錄下的express文件。

module.exports = require('./lib/express');

通過測試前兩行可以判斷l(xiāng)ib/express.js導(dǎo)出的結(jié)果應(yīng)該是一個函數(shù)脊另,所以在express.js文件中添加如下代碼:

function createApplication() {
  return {};
}

exports = module.exports = createApplication;

測試程序中包含兩個函數(shù)导狡,所以暫時將createApplication函數(shù)體現(xiàn)如下:

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

    listen: function() {
      console.log('express().listen function');
    }
  }
}

雖然無法得到想要的結(jié)果,但至少可以將測試程序跑通偎痛,函數(shù)的核心內(nèi)容可以在接下來的步驟中不斷完善旱捧。至此,初始框架搭建完畢踩麦,測試test/index.js文件的前兩行:

const express = require('../lib/express');
const app = express();

運行node test/index.js查看結(jié)果

2.第一次迭代

本節(jié)是express的第一次迭代枚赡,主要的目標是將當前的測試用例功能完整的實現(xiàn),一共分兩部分:

  • 實現(xiàn)http服務(wù)器靖榕。
  • 實現(xiàn)get路由請求标锄、
    實現(xiàn)http服務(wù)器比較簡單,參考node.js官網(wǎng)實現(xiàn):
const http = require('http');

const hostname = '127.0.0.1';
const post = 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}`);
});

參考案例茁计,實現(xiàn)express的lsiten函數(shù):

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

  return server.listen(port, cb);
}

當前l(fā)isten函數(shù)包含兩個參數(shù)料皇,但是http.listen有很多重載函數(shù),為了和http.listen一致星压,可以將函數(shù)設(shè)置為http.listen'代理'践剂,這樣可以保持express().listen和http.listen的參數(shù)統(tǒng)一。

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

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

在listen函數(shù)中娜膘,我們攔截了所有http請求逊脯,每次http請求都會打印http.createServer...,之所以攔截http請求竣贪,是因為express需要分析每次http請求军洼,根據(jù)http請求的不同來處理不同的業(yè)務(wù)邏輯。
在底層:
一個http請求主要包括請求行演怎、請求頭和消息體匕争,node.js將常用的數(shù)據(jù)封裝為http.IncomingMessage類,在上面的代碼中req就是該類的一個對象爷耀。
每個http請求都會對應(yīng)一個http響應(yīng)甘桑,一個http響應(yīng)主要包括:狀態(tài)行、響應(yīng)頭歹叮、消息跑杭,node.js將常用的數(shù)據(jù)封裝為http.ServerResponse類,在上面的代碼中res就是該類的一個對象咆耿。
不僅僅是node.js德谅,基本上所有的http服務(wù)框架都包含request和response兩個對象,分別代表著http的請求和響應(yīng)萨螺,負責(zé)服務(wù)端和瀏覽器的交互窄做。
在上層:
服務(wù)器后臺代碼根據(jù)http請求的不同宅荤,綁定不同的邏輯,在真正的http請求到來時浸策,匹配這些http請求,執(zhí)行與之對應(yīng)的邏輯惹盼,這個過程就是web服務(wù)器的執(zhí)行流程庸汗。
對于這些http請求的管理,有一個專有名詞:路由管理手报,每個http請求就默認為一個路由蚯舱,常見的路由策略包括:URL、HTTP請求名詞等等掩蛤,單不僅僅限定這些枉昏,所有http請求頭上的參數(shù)其實都可以進行判斷區(qū)分,例如使用user-agent字段來判斷移動端揍鸟。
不同的框架對于路由的管理策略不同兄裂,但不管怎樣,都需要一組管理http請求和業(yè)務(wù)邏輯映射的函數(shù)阳藻,測試用例中的get函數(shù)就是路由管理中的一個函數(shù)晰奖,主要負責(zé)添加get請求。
既然知道路由管理的重要腥泥,這里就創(chuàng)建一個router數(shù)組匾南,負責(zé)管理所有路由映射,參考express框架蛔外,抽象出每個路由的基本屬性:

  • path請求路徑:例如:/books蛆楞、/books/1。
  • method請求方法:例如:GET夹厌、POST豹爹、PUT、DELETE尊流。
  • handle處理函數(shù):
var router = [{
  path: '*',
  method: '*',
  handle: function(req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('404');
  }
}];

修改listen函數(shù)帅戒,將http請求攔截改為匹配router路由表,如果匹配成功崖技,執(zhí)行對應(yīng)的handle函數(shù)逻住,否則執(zhí)行router[0].handle函數(shù)。

listen: function(port, cb) {
  var server = http.craeteServer(function() {

    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);
}

實現(xiàn)get路由請求非常簡單迎献,該函數(shù)主要是添加get請求路由瞎访,只需要將其信息加入到router數(shù)組即可。

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

執(zhí)行測試用例時吁恍,報錯扒秸,提示res.send不存在播演。該函數(shù)并不是node.js原生函數(shù),這里在res上臨時添加函數(shù)send伴奥,負責(zé)發(fā)送響應(yīng)到瀏覽器写烤。

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

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

在技術(shù)這次迭代之前,拆分整理一下程序目錄:
創(chuàng)建application.js文件拾徙,將createApplication函數(shù)中的代碼轉(zhuǎn)移到該文件洲炊,express.js文件只保留引用。

var app = require('./application');
function createApplication() {
  return app;
}

exports = module.exports = createApplication;

整個目錄結(jié)構(gòu)如下:

express
  |
  |-- lib
  |    | 
  |    |-- express.js
  |    |-- application.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js

最后尼啡,運行 node test/index.js暂衡,打開瀏覽器訪問http://127.0.0.1:3000/。

3.第二次迭代

本節(jié)是express的第二次迭代崖瞭,主要目標是構(gòu)建一個初步的路由系統(tǒng)狂巢。根據(jù)上一節(jié)的改動,目前的路由是用一個router數(shù)組進行描述管理书聚,對于router的操作有兩個唧领,分別是:application.get函數(shù)和application.listen函數(shù),前者用于添加雌续,后者用于處理疹吃。
按照面向?qū)ο蟮姆庋b法則,接下來將路由系統(tǒng)的數(shù)據(jù)和路由系統(tǒng)的操作封裝到一起定義一個Router類負責(zé)整個路由系統(tǒng)的主要工作:

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文件的內(nèi)容:

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本身有關(guān)萨驶,而與application分離,使代碼更加清晰艇肴。
這個版本路由系統(tǒng)正常運行沒有問題腔呜,但是隨著路由不斷增多,this.stack數(shù)組會不斷地增大再悼,匹配效率會不斷降低核畴,為了解決效率的問題,需要分析路由的組成成分冲九。
目前在express中谤草,一個路由是由三部分組成:路徑、方法和處理函數(shù)莺奸。前兩者的關(guān)系并不是一對一關(guān)系丑孩,而是一對多關(guān)系,如:

GET books/1
PUT books/1
DELETE books/1

如果將路徑一樣的路由整合成一組灭贷,顯然效率會提高很多温学,于是引入Layer的概念。
這里將Router系統(tǒng)中this.stack數(shù)組中的每一項甚疟,代表一個Layer仗岖。每個Layer內(nèi)部包含3個變量:

  • path, 表示路由的路徑
  • handle逃延,代表路由的處理函數(shù)
  • router,代表真正的路由
    整體結(jié)構(gòu)如下圖所示:
------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| Layer     | Layer     | Layer     | Layer     |
|  |- path  |  |- path  |  |- path  |  |- path  |
|  |- handle|  |- handle|  |- handle|  |- handle|
|  |- route |  |- route |  |- route |  |- route |
------------------------------------------------
                  router 內(nèi)部

這里先創(chuàng)建Layer類:

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

//  簡單處理
Layer.prototype.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.write(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,這樣的結(jié)果導(dǎo)致如果測試用例存在問題:

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

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

程序無法分清PUT和GET的區(qū)別檩电。
所以需要繼續(xù)完善路由系統(tǒng)中的Layer類的route屬性盔然,這屬性才是真正包含method屬性的路由。route結(jié)構(gòu)如下:

------------------------------------------------
|     0     |     1     |     2     |     3     |      
------------------------------------------------
| item      | item      | item      | item      |
|  |- method|  |- method|  |- method|  |- method|
|  |- handle|  |- handle|  |- handle|  |- handle|
------------------------------------------------
                  route 內(nèi)部

創(chuàng)建Route類:

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

  this.method = {};
};

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

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

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

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 slef.stack[i].handle_request(req, res);
    }
  }
};

在上面的代碼中是嗜,斌沒有定義前面結(jié)構(gòu)圖中的item對象,而是使用了Layer對象進行替代挺尾,主要是為了方便快捷鹅搪,從另一個角度來看,其實而這存在很多共同點遭铺。另外丽柿,為了利于理解,代碼只實現(xiàn)了GET方法魂挂,其他方法的代碼實現(xiàn)是類似的甫题、既然有了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 Router(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,一切看起來和原來一樣果正。
這節(jié)內(nèi)容是創(chuàng)建一個完整的路由系統(tǒng)炎码,并在原始代碼的基礎(chǔ)上引入了Layer和Route兩個概念,并修改了大量的代碼秋泳,在技術(shù)本節(jié)之前總結(jié)一下目前的信息潦闲。

express
  |
  |-- lib
  |    | 
  |    |-- express.js
  |    |-- application.js
  |    |-- router
  |          |
  |          |-- index.js
  |          |-- layer.js
  |          |-- route.js
  |
  |-- test
  |    |
  |    |-- index.js
  |
  |-- index.js

接著,總結(jié)了一下當前express各個部分的工作:
application代表一個應(yīng)用程序迫皱,express是一個工廠類來負責(zé)創(chuàng)建application對象歉闰。Router代表路由組件,負責(zé)應(yīng)用程序的整個路由系統(tǒng)卓起。組件內(nèi)部由一個layer數(shù)組構(gòu)成和敬,每個Layer代表一組路徑相同的路由信息国葬,具體信息存儲在Route內(nèi)部勘伺,每個Route內(nèi)部也是一個Layer對象,但是Route內(nèi)部的Layer和Router內(nèi)部的Layer是存在一定差異性的芍碧。

  • Router內(nèi)部的Layer饲握,主要包含path私杜、route屬性蚕键。
  • Route內(nèi)部的Layer,主要包含method衰粹、handle屬性锣光。
    如果一個請求來臨,會先從頭到尾掃描router內(nèi)部的每一層铝耻,而處理每層的時候會對比URI誊爹,相同則掃描route的每一項,匹配成功則返回具體的信息瓢捉,沒有任何匹配則返回未找到频丘。
    最后整個路由系統(tǒng)結(jié)構(gòu)如下:
 --------------
| 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.第三次迭代

本節(jié)是express的第三次迭代,主要目標是繼續(xù)完善路由系統(tǒng)泡态,主要工作有以下部分:

  • 豐富接口搂漠,目前只支持get接口。
  • 增加路由系統(tǒng)的流程控制某弦。
    當前express框架只支持get接口桐汤,具體的接口時express提供的,內(nèi)部調(diào)用Router.get接口靶壮,而其內(nèi)部是對Route.get的封裝怔毛。
    HTTP顯然不僅僅只有GET這一個方法,還包括很多類型腾降,例如:PUT拣度、POST、DELETE等等螃壤,每個方法單獨寫一個處理函數(shù)顯然是冗余的蜡娶,因為函數(shù)的內(nèi)容除了與函數(shù)名相關(guān)外,其他是一成不變的映穗。根據(jù)JavaScript腳本語言的特性窖张,這里可以通過HTTP的方法列表動態(tài)生成函數(shù)內(nèi)容。
    想要動態(tài)生成函數(shù)蚁滋。
    想要動態(tài)生成函數(shù)宿接,首先需要確定函數(shù)名稱。函數(shù)名就是node.js中HTTP服務(wù)器支持的方法名稱辕录,可以在官方文檔中獲取睦霎,具體參數(shù)是:http.METHODS。這個屬性是v0.11.8新增的走诞,如果node.js地獄該版本副女,需要手動建立一個方法列表,具體可以參考node.js代碼蚣旱。
    express框架HTTP方法名的獲取封裝到另一個包碑幅,叫做methods戴陡,內(nèi)部給出了低版本的兼容動詞列表。
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'
  ];
}

直到所支持的方法名列表數(shù)組后沟涨,剩下的只需要一個for循環(huán)生成所有的函數(shù)即可恤批。所有的動詞處理函數(shù)的核心內(nèi)容都在Router中。

http.METHODS.forEach(function(method) {
  method = method.toLowerCase();
  Route.prototype[method] = function() {
      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的內(nèi)容裹赴,這里改動較大喜庞,重新定義了一個Application類,而不是使用字面量直接創(chuàng)建棋返。

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方法函數(shù):

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

因為導(dǎo)出的是Application類延都,所以修改express.js文件

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

function createApplication() {
  var app = new Application();
  return app;
}

運行node test/index.js。
如果認真研究路由系統(tǒng)的源碼睛竣,會發(fā)現(xiàn)route本身的定義并不和我們的文字描述那樣晰房。例如:增加兩個同樣的路由:

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

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

并不像我們所設(shè)想的類似于以下結(jié)構(gòu):

                          ---------------------------------------------------------
 ----- -----------        |     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

實際上是如下結(jié)構(gòu):

 ----- -----------        -------------
|     | 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            

之所以會這樣時因為路由系統(tǒng)存在先后順序的關(guān)系,前面的描述結(jié)構(gòu)很有可能丟失順序這個屬性酵颁。既然這樣,我們要思考Route的用處所在月帝。
因為express框架中躏惋,Route存儲的是真正的路由信息,可以當作單獨的成員使用嚷辅,如果想要真正得到前面所描述的結(jié)果簿姨,需要如下創(chuàng)建自己的代碼:

var router = express.Router();

router.route('/user/: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 = express();

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

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

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

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

理解了Route的使用方法,接下來就要討論剛剛提到的順序問題簸搞。在路由系統(tǒng)中扁位,路由的處理順序非常重要,因為1路由是按照數(shù)組的方式存儲的趁俊,如果遇到兩個同樣的路由域仇,同樣的方法名,不同的處理函數(shù)寺擂,這時候前后聲明的順序?qū)⒅苯佑绊懡Y(jié)果(這也是express中間件存在順序相關(guān)的原因)暇务,例如下面的例子:

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');
});

上面的代碼如果執(zhí)行會發(fā)現(xiàn)永遠都返回first,但是有時候會根據(jù)前臺傳來的參數(shù)動態(tài)判斷是否執(zhí)行接下來的路由怔软,怎樣才能跳過first進入second垦细?這就涉及到路由系統(tǒng)的流程控制問題。
流程控制分為主動和被動兩種模式:
對于express框架來說挡逼,路由綁定的處理邏輯括改、用戶設(shè)置的路徑參數(shù)這些都是不可靠的,在運行過程中很有可能會發(fā)生異常家坎,被動流程控制就是當這些異常發(fā)生的時候嘱能,express框架要負擔起捕獲這些異常的工作吝梅,因為如果不明確異常發(fā)生的位置,會導(dǎo)致js代碼無法繼續(xù)運行焰檩,并且無法精確地報出故障憔涉。
主動流程控制則是處理函數(shù)內(nèi)部的操作邏輯,以主動調(diào)用的方式來跳轉(zhuǎn)路由內(nèi)部的執(zhí)行邏輯析苫。
目前express通過引入next參數(shù)的方式來解決流程控制問題兜叨。next是處理函數(shù)的一個參數(shù),其本身也是一個函數(shù)衩侥,該函數(shù)有幾種使用方式:

  • 執(zhí)行下一個處理函數(shù)国旷。執(zhí)行next()。
  • 報告異常茫死。執(zhí)行next(err)跪但。
  • 跳過當前的Route,執(zhí)行Route的下一項峦萎。執(zhí)行next('route')屡久。
  • 跳過整個Router。執(zhí)行next('router')爱榔。
    接下來被环,我們嘗試實現(xiàn)以下這些需求:
    首先,修改最底層的Layer對象详幽,該對象的handle_request函數(shù)是負責(zé)調(diào)用路由綁定的處理邏輯筛欢,這里添加next函數(shù),并添加異常捕獲功能唇聘。
Layer.prototype.handle_request = function(req, res, next) {
  var fn = this.handle;

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

接下來修改Route.dispatch函數(shù)版姑。因為涉及內(nèi)部的邏輯跳轉(zhuǎn),使用for循環(huán)按部就班不太合適迟郎,這里使用了類似于遞歸的方式:

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(null);
    }

    //  跳過整個路由系統(tǒng)
    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();
};

整個處理過程本質(zhì)上是一個for循環(huán)剥险,唯一的差別就是在處理函數(shù)中主動調(diào)用next函數(shù)的處理邏輯。如果用戶通過next函數(shù)返回錯誤宪肖、route和router這三種情況炒嘲,目前統(tǒng)一拋給Router處理。因為修改了dispatch函數(shù)匈庭,所以調(diào)用該函數(shù)的Router,router函數(shù)也要修改夫凸,這次直接徹底修改,以后無需根據(jù)參數(shù)的個數(shù)進行調(diào)整阱持。

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);

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

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

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

修改后的函數(shù)處理過程和原來的類似,不過有一點需要注意鸽扁,當發(fā)生異常的時候蒜绽,會將結(jié)果返回給上一層,而不是執(zhí)行原有的this.stack第0層的代碼邏輯桶现。
最后增加express框架異常處理的邏輯躲雅。
在之前,可以移除原有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函數(shù):

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函數(shù)處理為404頁面,其實在express框架中慰于,使用的是一個單獨的npm包钮科,叫做finalhandler。
簡單的修改一下測試用例證明一下成果:

var express = require('../lib/express');
var app = express();

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/绵脯,結(jié)果顯示:

404: Error: error

貌似目前一切都很順利,不過還有一個需求目前被忽略了休里。當前處理函數(shù)的異常全部是由框架捕獲蛆挫,返回的信息只能是固定的404頁面,對框架使用者顯然很不方便妙黍,大多時候悴侵,我們希望可以捕獲錯誤,并按照一定的信息封裝返回瀏覽器废境,所以express需要一個返回錯誤給上層使用者的接口畜挨。
目前和上層對接的處理函數(shù)的聲明如下:

function process_fun(req, res, next) {  
}

如果增加一個錯誤處理函數(shù)筒繁,按照node,js的規(guī)則噩凹,第一參數(shù)是錯誤信息,定義應(yīng)該如下所示:

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

因為兩個聲明的第一個參數(shù)信息時不同的毡咏,如果區(qū)分傳入的處理函數(shù)是處理錯誤的函數(shù)還是處理正常的函數(shù)驮宴,這個是express框架要搞定的關(guān)鍵問題。
javascript中呕缭,F(xiàn)unction.length屬性可以獲取傳入函數(shù)指定的參數(shù)個數(shù)堵泽,這個可以當做區(qū)分二者的關(guān)鍵信息。既然確定了原理恢总,接下來直接在Layer類上增加一個專門處理錯誤的函數(shù)迎罗,和處理正常新的函數(shù)區(qū)分開。

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

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

  try {
    fn(error, req, res, next)
  } catch(err) {
    next(err);
  }
};

接著修改Route.dispatch函數(shù):

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();   
};

當發(fā)生錯誤的時候纹安,Route會一直向后尋找錯誤處理函數(shù),如果找到則返回,否則執(zhí)行done(err)厢岂,將錯誤拋給Router光督。對于Router.handle的修改,因為涉及一些中間件的概念塔粒,完整的錯誤處理將移到下一節(jié)完成结借。本節(jié)的內(nèi)容基本上完成,包括HTTP相關(guān)的動作接口的添加卒茬、路由系統(tǒng)的跳轉(zhuǎn)以及Route級別的錯誤響應(yīng)等等船老,涉及到路由系統(tǒng)剩下的一點內(nèi)容,暫時放到以后講解扬虚。

4.第四次迭代

本節(jié)是express的第四次迭代努隙,主要目標是建立中間件機制并繼續(xù)完善路由系統(tǒng)的功能。
在express中辜昵,中間件其實是一個介于web請求來臨后到調(diào)用處理函數(shù)前整個流程體系中間調(diào)用的組件荸镊。起本質(zhì)是一個函數(shù),內(nèi)部可以訪問修改請求和響應(yīng)對象堪置,并調(diào)整接下來的處理流程躬存。
express官方給出的解釋如下:
Express是一個資深功能極簡,完全是路由和中間件構(gòu)成的一個web開發(fā)框架:從本質(zhì)上來說舀锨,一個Express應(yīng)用就是在調(diào)用各種中間件岭洲。
中間件(Middleware)是一個函數(shù),他可以訪問請求對象(request object(req))坎匿,響應(yīng)對象(response object(res))盾剩,和web應(yīng)用中處于請求-響應(yīng)循環(huán)流程中的中間件,一般被命名為next的變量替蔬。
中間件的功能包括:

  • 執(zhí)行任何代碼告私。
  • 修改請求和響應(yīng)對象。
  • 終結(jié)請求-響應(yīng)循環(huán)承桥。
  • 調(diào)用堆棧中的下一個中間件驻粟。
    如果當前中間件沒有終結(jié)請求-響應(yīng)循環(huán),則必須調(diào)用next()方法將控制權(quán)交給下一個中間件凶异,否則請求就會掛起蜀撑。Express應(yīng)用可使用如下幾種中間件:
  • 應(yīng)用級中間件
  • 路由級中間件
  • 錯誤處理中間件
  • 內(nèi)置中間件
  • 第三方中間件
    使用可選擇掛載路徑,可在應(yīng)用級別或路由級別裝載中間件剩彬。另外酷麦,你還可以同時裝載一系列中間件函數(shù),從而在一個掛載點上創(chuàng)建一個子中間件棧喉恋。
    官方給出的定義其實已經(jīng)足夠清晰沃饶,一個中間件的樣式其實就是上一節(jié)所提到的處理函數(shù)粪摘,只不過并沒有正式命名。所以對于代碼來說Router類中的this.stack屬性內(nèi)部的每個handle都是一個中間件绍坝,根據(jù)使用接口不同區(qū)別了應(yīng)用級中間件和路由級中間件徘意,而四個參數(shù)的處理函數(shù)就是錯誤處理中間件。
    接下來就給express框架添加中間件功能轩褐。
    首先是應(yīng)用級中間件椎咧,其使用方法是Application類上的兩種方式:Application.use和Application.METHOD(HTTP的各種請求方法),后者其實在前面的小節(jié)已經(jīng)實現(xiàn)了把介,前者則是需要新增的勤讽。
    在前面的小節(jié)里代碼已經(jīng)說明了Application.METHOD內(nèi)部其實是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框架支持的參數(shù)不僅僅這兩種,但是為了便于理解剔除一些旁枝末節(jié)巢墅,便于框架的理解诸狭,接下來實現(xiàn)Router.use函數(shù)。

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;
};

內(nèi)部代碼和Application.use差不多君纫,只不過最后不再是調(diào)用Router.use驯遇,而是直接創(chuàng)建一個Layer對象,將其放到this.stack數(shù)組中蓄髓。
在這段代碼中可以看到普通路由與中間路由的區(qū)別叉庐。普通路由放到Route中,且Router.route屬性指向Route對象会喝,Router.handle屬性指向Route.dispatch函數(shù)陡叠;中間件的Route.route屬性為undefined,Router.handle指向中間件處理函數(shù)肢执,被放到Router.stack數(shù)組中枉阵。
對于路由級中間件,首先按照要求導(dǎo)出Router類蔚万,便于使用岭妖。

exports.Router = Router;

上面的代碼添加到express.js文件中临庇,這樣就可以按照下面的使用方式創(chuàng)建一個單獨路由系統(tǒng)反璃。

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

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

現(xiàn)在問題來了,如果像上面的代碼一樣創(chuàng)建一個新的路由系統(tǒng)無法讓路由系統(tǒng)內(nèi)部的邏輯生效假夺,因為這個路由系統(tǒng)沒法添加到現(xiàn)有的系統(tǒng)中淮蜈。
一種方法是添加一個專門添加新路由系統(tǒng)的接口,這是完全可行的已卷,但是更好的方式是按照express框架的方式梧田,這可能是Router叫做路由級中間件的原因。express將Router定義成一個特殊的中間件,而不是一個單獨的類裁眯。
這樣將單獨創(chuàng)建的路由系統(tǒng)添加到現(xiàn)有的應(yīng)用中的代碼將會非常簡單通用:

var router = express.Router();

//  將路由掛載至應(yīng)用
app.use('/', router);

這確實是一個好方法鹉梨,現(xiàn)在就將express改造成類似的樣子、
首先創(chuàng)建一個原型對象穿稳,將現(xiàn)有的Router方法轉(zhuǎn)移到該對象上存皂。

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, fn);
    route[method].call(route, fn);

    return this;
  };
});

然后創(chuàng)建一個中間件函數(shù),使用Object.setPrototypeOf函數(shù)設(shè)置其原型逢艘,最后導(dǎo)出一個生成這些過程的函數(shù)旦袋。

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.noew());
  next();
});

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

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

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

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

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

結(jié)果并不理想它改,原有的應(yīng)用程序還有兩個地方需要修改疤孕。
首先是邏輯處理問題。原有的Router.handle函數(shù)并沒有處理中間件的情況央拖,需要進一步修改祭阀。

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) {
    ...
    // 匹配鲜戒,執(zhí)行
    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, nexr);
    }
  }

  next();
};

其次是路徑匹配的問題柬讨。原有單一路徑被拆分成為不同中間件的路徑組合,這里需要判斷需要分多步進行袍啡,因為每個中間件只是匹配自己的路徑是否通過踩官,不過相對而言目前設(shè)計的匹配都是全等匹配,還沒有涉及到類似于express框架中的正則匹配境输,算是經(jīng)過一定簡化了蔗牡。
想要實現(xiàn)匹配邏輯就要清楚的知道哪段路徑和哪個處理函數(shù)匹配,這里定義三個變量:

  • req.originalUrl 原始請求路徑嗅剖。
  • req,url 當前路徑辩越。
  • req,baseUrl 父路徑。
    主要修改Router.handle函數(shù)信粮,該函數(shù)主要負責(zé)提取當前路徑段黔攒,便于和事先傳入的路徑進行匹配。
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.originalUrl = req.originalUrl || req.url;

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

    // 如果有移除强缘,復(fù)原原有路徑
    if(slashAdded) {
      req.url = '';
      slashAdded = false;
    }

    // 如果有移除督惰,復(fù)原原有路徑信息
    if(remoe.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = removed + req.url;
      remove = '';
    }

    // 跳過路由系統(tǒng)
    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++];
    // 匹配,執(zhí)行
    if(layer.match(path)) {
      // 處理中間件
      if(!layer.route) {
        // 要移除的部分路徑
        removed = layer.path;

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

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

        // 調(diào)用處理函數(shù)
        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('/user', router);

這種情況下,Router.handle順序匹配到中間的時候商虐,會遞歸調(diào)用Router.handle觉阅,所以需要保存當前的路徑快照崖疤,具體路徑相關(guān)信息放到req.url,req.originalUrl和req.baseUrl這三個參數(shù)中典勇。
第二種劫哼,非路由中間件的情況,如:

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

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

這種情況下割笙,Router.handle內(nèi)部主要是按照棧中的次序匹配路徑即可沦偎。
改好了處理函數(shù),還需要修改一下Layer.match這個匹配函數(shù)咳蔚。目前創(chuàng)建的Layer可能會有三種情況:

  • 不含有路徑的中間件豪嚎。path屬性默認為/。
  • 含有路徑的中間件谈火。
  • 普通路由侈询。如果path屬性為,表示任意路徑糯耍。
    修改原有Layer是構(gòu)造函數(shù)扔字,增加一個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匹配函數(shù):

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;
};

代碼中一共判斷四種情況,根據(jù)this.route區(qū)分中間件和普通路由舵鳞,然后分開判斷震檩。
express除了普通的中間件外還要一種錯誤中間件,專門來處理錯誤信息蜓堕。該中間件的聲明和上一小節(jié)最后介紹的錯誤處理函數(shù)是一樣的抛虏,同樣是四個參數(shù)分別為:err, req, res, next。
目前Router.handle中套才,當遇到錯誤信息的時候迂猴,會直接通過回調(diào)函數(shù)返回錯誤信息,顯示錯誤頁面背伴。

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

這里需要修改策略沸毁,將其改為繼續(xù)調(diào)用下一個中間件,直到碰到錯誤中間件為止傻寂。

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

原有這一部分的代碼只保留判斷枚舉是否完成息尺,將錯誤判斷轉(zhuǎn)移到最后執(zhí)行處理函數(shù)的為止。之所以這樣做是因為不管是執(zhí)行處理函數(shù)崎逃,或是執(zhí)行錯誤處理都需要進行路徑匹配操作和路徑分析操作掷倔,所以放到后面正好合適眉孩。

// 處理中間件
if(!layer.route) {
  ...
  // 調(diào)用處理函數(shù)
  if(layerError)
    layer.hand_error(layerError, req, res, next);
  else
    layer.handle_request(req, res, next);
} else if(layer.route._handle_method(method)) {
  // 處理路由
  layer.handle_request(req, res, next);
}

到此為止个绍,express的錯誤處理部分算是基本完成了勒葱。
路由系統(tǒng)和中間件兩個大概念基本講解完畢,當然還有很多細節(jié)沒有完善巴柿。在剩下的文字里會繼續(xù)完善凛虽。
下一節(jié)的主要內(nèi)容是介紹前后端交互的兩個重要成員:request和response。express在node.js的基礎(chǔ)上進行了豐富的擴展广恢。

5.第五次迭代

本節(jié)是express的第五次迭代凯旋,主要的目標是封裝request和response兩個對象,方便使用钉迷。
其實node.js已經(jīng)給我們提供了兩個默認對象至非,之所以要封裝是因為豐富一下二者的接口,方便框架使用者糠聪,目前框架在response對象上已經(jīng)有一個接口:

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

如果需要繼續(xù)封裝荒椭,也要類似的結(jié)構(gòu)在代碼上添加顯然給人一種很亂的感覺,因為request和response的原始版本是node.js提供給框架的舰蟆,框架獲取到的是兩個對象趣惠,并不是類,要想在而浙商提供另一組接口的辦法有很多身害,規(guī)格芥蒂就是將新的接口加到該對象上或者該對象的原型鏈上味悄,目前的代碼選擇了前者,express的代碼選擇了后者塌鸯。
首先建立兩個文件:request.js和response.js侍瑟,二者分別導(dǎo)出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;

二者文件的代碼都是創(chuàng)建一個對象丙猬,分別指向node.js提供的request和response兩個對象的原型丢习,以后express自定的接口可以統(tǒng)一掛載到兩個對象上。
接著修改Application.handle函數(shù)淮悼,因為這個函數(shù)里面有新鮮出爐的request和response咐低。思路很簡單,就是將二者的原型指向我們自建的req和res袜腥。因為req和res對象的原型和request和response的原型相同见擦,所以并不影響原有node.js的接口。

var request = require('./request');
var response = require('./response');
...
Application.prototype.handle = function(req, res) {
  object.setPrototypeOf(req, request);
  object.setPrototypeOf(res, response);
  ...
};

這里將原有的res.send轉(zhuǎn)移到了response.js文件中:

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

注意函數(shù)中不再是res.writeHead和res.end羹令,而是this.wirteHead和this.end鲤屡。
在整個路由系統(tǒng)中,Router.stack每一項其實都是一個中間件福侈,每個中間件都有可能用到req和res兩個對象酒来,所以代碼中修改node.js原生提供的request和response對象的代碼放到了Application.handle中,這樣做并沒有問題肪凛,但是有一種更好的方式堰汉,express框架將這部分代碼封裝成了一個內(nèi)部中間件辽社。
為了確保框架中每個中間件接收這兩個參數(shù)的正確性翘鸭,需要將內(nèi)部中間件放到Router.stack的首項滴铅。這里將原有Application的構(gòu)造函數(shù)中的代碼去掉,不再是直接創(chuàng)建Router()路由系統(tǒng)就乓,而是用一種惰性懶加載的方式汉匙,動態(tài)創(chuàng)建。
去除原有Application構(gòu)造函數(shù)的代碼:

function Application() {}

添加惰性初始化函數(shù):

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

    this._router.use(middleware.init());
  }
};

因為是惰性初始化生蚁,所以在使用this._router對象前噩翠,一定要先調(diào)用該函數(shù)動態(tài)創(chuàng)建this._router對象。類似于如下代碼:

// 獲取router
this.lazyrouter();
router = this._router;

接下來創(chuàng)建一個叫middleware的文件夾邦投,專門放置內(nèi)部中間件的文件绎秒,在創(chuàng)建一個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.setPrototype(req, request);
  Object.setPrototype(res, response);

  // 繼續(xù)
  next();
};

修改原有的Application.handle函數(shù):

Application.prototype.handle = function(req, res) {
  ...
  // 這里無需調(diào)用lazyrouter见芹,因為listen前一定調(diào)用了.use或者.METHODS方法轧铁。
  // 如果二者沒有調(diào)用罕邀,沒有必要創(chuàng)建路由it。this._router為undefined缨睡。
  var router = this._router;
  if(router) {
    router.handle(req, res, done);
  } else {
    done();
  }
};

運行node test/index.js......
express框架中和二,request和response兩個對象有很多非常好用的函數(shù)徘铝,不過大部分和框架結(jié)構(gòu)無關(guān),并且對這些函數(shù)內(nèi)部過于專研細節(jié)惯吕,對框架本身的理解并沒有多少幫助惕它。不過接下來有一各方面需要仔細研究一下,那就是前后端參數(shù)的傳遞废登,express如何獲取并分類這些參數(shù)淹魄,這一點還是需要稍微了解的。
默認情況下堡距,一共有三種參數(shù)獲取方式:

  • req.query代表查詢字符串甲锡。
  • req.params代表路徑變量。
  • req.body代表表單提交變量羽戒。
    req.query是最常用的方式缤沦,例如:
// GET /search?q=tobi+ferret
req.query.q 
// => "tobi ferret"

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

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

req.query.shoe.type
// => "converse"

后臺獲取這些參數(shù)最簡單的方式就是通過node.js自帶的querystring模塊分析URL。express使用的另一個npm包易稠,qs缸废。并且將其分裝為另一個內(nèi)部中間件,專門負責(zé)解析查詢字符串,默認加載企量。
req.params是另一種從URL獲取參數(shù)的方式测萎,例如:

// 路由骨子額 /user/:name
// GET /user/tj
req.params.name
// => "tj"

這是一種express框架規(guī)定的參數(shù)獲取方式,對于批量處理邏輯非常實用梁钾。在我們的express中并沒有實現(xiàn)绳泉,因為路徑匹配問題過于細節(jié)化逊抡,如果對此感興趣可以研究path-to-regexp模塊姆泻,express也是在其上的封裝。
req.body是獲取表單數(shù)據(jù)的方式冒嫡,但是默認情況下框架是不會去解析這種數(shù)據(jù)拇勃,直接使用只會返回undefined。如果想要支持需要添加其他中間件孝凌,例如:body-parser或multer方咆。
本小節(jié)主要介紹了request和response兩個對象,而且講解了一下現(xiàn)有express框架中獲取參數(shù)的方式蟀架,整體上并沒有太深入的防治瓣赂,主要是這方面設(shè)計細節(jié)過多。

6.第六次迭代

本小節(jié)是第六次迭代片拍,主要目的是介紹一下express是如何集成現(xiàn)有的渲染引擎煌集。與渲染引擎有關(guān)的事情涉及到下面幾個方面:

  • 如何開發(fā)或綁定一個渲染引擎。
  • 如何注冊一個渲染引擎捌省。
  • 如何指定模板路徑苫纤。
  • 如何渲染模板引擎。
    express通過app.engine(ext, callback)方法創(chuàng)建一個你自己的模板引擎纲缓。其中卷拘,ext指的是文件擴展名,callback是末班引擎的主函數(shù)祝高,接受文件路徑栗弟、參數(shù)對象和回調(diào)函數(shù)作為其參數(shù)。
// 下面的代碼演示的是一個非常簡單的能夠渲染".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);
  })
});

為了讓應(yīng)用程序可以渲染模板文件横腿,還可以做如下設(shè)置:

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

一旦view engine設(shè)置成功,就不需要顯示制定引擎斤寂,或者在應(yīng)用中加載引擎模塊耿焊,Express已經(jīng)在內(nèi)部加載。下面是如何渲染頁面的方法:

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

想要實現(xiàn)上述功能遍搞,首先在Application類中定義兩個變量罗侯,一個存儲app.set和app.get這兩個方法存儲的值,另一個存儲模板引擎中的擴展名和渲染函數(shù)的對應(yīng)關(guān)系溪猿。

function Application() {
  this.settings = {};
  this.engines = {};
}

然后是實現(xiàn)app.set函數(shù):

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

  this.settings[setting] = val;
  return this;
};

代碼不僅僅實現(xiàn)了設(shè)置钩杰,而且當傳入的參數(shù)只要一個時等價于get函數(shù)纫塌。
接著實現(xiàn)app.get函數(shù)。因為現(xiàn)在已經(jīng)有一個app.get方法來設(shè)置路由讲弄,所以需要在該方法上進行重載措左。

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

最后實現(xiàn)app.engine進行擴展名和引擎函數(shù)的映射。

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

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

  return this; 
};

擴展名當做key避除,統(tǒng)一添加"."怎披。
到此設(shè)置模板相關(guān)信息的函數(shù)部分算是完成,接下來就是最重要的渲染引擎函數(shù)的實現(xiàn)瓶摆。

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

  // 如果定義回調(diào)凉逛,則返回,否則渲染
  done = done || function(err, str) {
    if(err) {
      return req.next(err);
    }

    self.send(str);
  };

  // 渲染
  app.render(view, opts, done);
};

渲染函數(shù)一共有三個參數(shù):view表示模板的名稱群井,options是模板渲染的變量状飞,callback是渲染成功后的回調(diào)函數(shù)。
函數(shù)內(nèi)部直接調(diào)用render函數(shù)進行渲染书斜,渲染完成后調(diào)用done回調(diào)诬辈。
接下來創(chuàng)建一個view.js文件,主要是負責(zé)各種模板引擎和框架間的隔離荐吉,保持對內(nèi)接口的統(tǒng)一性焙糟。

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

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

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

  vat 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類內(nèi)部定義了很多屬性,主要包括:引擎稍坯、根目錄酬荞、擴展名、文件名等等瞧哟,為了以后的渲染做準備混巧。

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

View的渲染函數(shù)內(nèi)部就是調(diào)用一開始注冊的引擎渲染函數(shù)。
了解了View的定義勤揩,接下來實現(xiàn)app.render模板渲染函數(shù)咧党。

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 enine'),
    root: this.get('views'),
    engines: engines
  });

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

  try {
    view.render(options, callback);
  } catch(e) {
    callback(e);
  }
};

修改測試用例,嘗試一下:

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陨亡,查看效果傍衡。
上面的代碼是自己注冊的引擎,如果想要和現(xiàn)有的模板引擎結(jié)合還需要在回調(diào)函數(shù)中引用模板自身的渲染方法负蠕,當然為了方便蛙埂,express框架內(nèi)部提供了一個默認方法,如果模板引擎導(dǎo)出了該方法遮糖,則表示該模板引擎支持express框架绣的,無需使用app.engine再次封裝。
該方法聲明如下:

__express(filePath, options, callback)

可以參考ejs模板引擎的代碼:

// 改代碼在lib/ejs.js文件中355行左右
exports.__express = exports.renderFile;

express為了實現(xiàn)這個默認加載功能,在View構(gòu)造函數(shù)中多加入一個判斷:

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

代碼很簡單屡江,如果沒有找到引擎對應(yīng)的渲染函數(shù)芭概,那就嘗試加載__express函數(shù)。

后記

擴展部分:
1.關(guān)于Application:
官方express源碼并沒有像本文一樣寫為一個類惩嘉,而是一個中間件罢洲,一個對象,該對象使用了mixin方法的多繼承手段文黎,express.js文件中的createApplication函數(shù)算是整個框架的切入點惹苗。
2.關(guān)于Router.handle:
這個函數(shù)可以說是整個express框架的核心,在仿制的時候舍棄了很多細節(jié)臊诊。函數(shù)內(nèi)部有2個關(guān)鍵點:一鸽粉,處理URL形式的參數(shù)斜脂,這里涉及對params參數(shù)的提取過程抓艳。其中有一個restore函數(shù)使用高階函數(shù)的方法做了緩存。二帚戳,setImmediate異步返回玷或,之所以使用異步處理,是因為下面的代碼需要運行片任,包括路徑相關(guān)的參數(shù)偏友,這些參數(shù)在下一個處理函數(shù)中可能會用到。
3.關(guān)于其他函數(shù):
涉及很多細節(jié):正則表達式对供,http協(xié)議層位他,node.js本身函數(shù)的使用。大部分函數(shù)都自成體系产场。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹅髓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子京景,更是在濱河造成了極大的恐慌窿冯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件确徙,死亡現(xiàn)場離奇詭異醒串,居然都是意外死亡,警方通過查閱死者的電腦和手機鄙皇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門芜赌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伴逸,你說我怎么就攤上這事缠沈。” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵博烂,是天一觀的道長香椎。 經(jīng)常有香客問我,道長禽篱,這世上最難降的妖魔是什么畜伐? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮躺率,結(jié)果婚禮上玛界,老公的妹妹穿的比我還像新娘。我一直安慰自己悼吱,他們只是感情好慎框,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著后添,像睡著了一般笨枯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上遇西,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天馅精,我揣著相機與錄音,去河邊找鬼粱檀。 笑死洲敢,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的茄蚯。 我是一名探鬼主播压彭,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渗常!你這毒婦竟也來了壮不?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤凳谦,失蹤者是張志新(化名)和其女友劉穎忆畅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尸执,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡家凯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了如失。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绊诲。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖褪贵,靈堂內(nèi)的尸體忽然破棺而出掂之,到底是詐尸還是另有隱情抗俄,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布世舰,位于F島的核電站动雹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏跟压。R本人自食惡果不足惜胰蝠,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望震蒋。 院中可真熱鬧茸塞,春花似錦、人聲如沸查剖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笋庄。三九已至效扫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間无切,已是汗流浹背荡短。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工丐枉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哆键,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓瘦锹,卻偏偏與公主長得像籍嘹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弯院,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容