這一節就講從一個請求到來,express內部是如何將其轉交給合適的路由,路由又是如何呼叫中介軟體的。

  以express-generator為例,關鍵程式碼如下:

// app.js
app.use('/', indexRouter);
app.use('/users', usersRouter);
// indexRouter
router.get('/', function(req, res, next) {
console.log('first middleware');
next();
},(req,res,next)=>{
res.render('index', { title: 'Express' });
});
// usersRouter
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});

  在兩個路由的JS中,兩次router.get呼叫會分別生成2個path層級的layer物件,中介軟體函式為內部方法route.dispatch,push進了router的stack陣列中,並掛載了2個route物件。而這兩個route物件根據後面的中介軟體函式數量又獨立生成了對應的內部layer,僅處理中介軟體函式,同時push到了route的stack中。

  在最外層的app.js中,呼叫app.use,傳入掛載路徑與返回的router物件,由於router物件沒有set方法,不是express應用,所以直接走的router.use方法。在use方法裡,生成了兩個Layer物件,路徑為app.use的第一個引數,fn為返回的router函式物件。

  最最後,2個Layer物件會被push進app的內部獨立router物件中。示意圖如下:

  提前簡單說一下涉及的四個模組app、router、layer、route。

1、app => 主要負責全域性配置引數讀取,所有的方法最終都會指向後面的工具模組,本身不做事

2、router => 所有app應用內部會有一個預設的router物件,該router物件上stack陣列中的Layer主要根據路徑把請求分發給處理對應路徑的自定義router。而自定義的router上layer物件也不會直接處理請求,而是再次根據路徑把請求分發給對應的route物件。route物件會遍歷stack陣列,依次取出layer呼叫中介軟體處理請求。

3、layer => 請求分發物件,雖然說3個引數分別為路徑、配置引數、處理函式,但是在實際情況中只會單獨處理一件事。

4、route => 最底層的物件,負責處理請求。

  這時候,假設有一個'/'根路徑的get請求過來了。

app.handle

  入口函式就是第一節就見過,但是一直沒有管的app.hanle:

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
// mixin && init
return app;
}
app.handle = function handle(req, res, callback) {
var router = this._router;
// 單app應用時為預設的finalhandler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
}); // no routes
if (!router) {
debug('no routes defined on app');
done();
return;
} router.handle(req, res, done);
};

  這裡假設只有一個app應用,請求進來後封裝了一個預設的callback,然後呼叫了router模組的handle方法。

router.handle

  這個函式太長了,分幾段來說吧。

proto.handle = function handle(req, res, out) {
var self = this; debug('dispatching %s %s', req.method, req.url); var idx = 0;
// 獲取請求地址的protocol + host
var protohost = getProtohost(req.url) || ''
var removed = '';
// 標記斜槓
var slashAdded = false;
var paramcalled = {}; // 應付OPTIONS方式的請求
var options = []; // 獲取本地的layer陣列
var stack = self.stack; // manage inter-router variables
var parentParams = req.params;
var parentUrl = req.baseUrl || '';
var done = restore(out, req, 'baseUrl', 'next', 'params'); // 掛載next方法
req.next = next; // options請求的預設返回
if (req.method === 'OPTIONS') {
done = wrap(done, function(old, err) {
if (err || options.length === 0) return old(err);
sendOptionsResponse(res, options, old);
});
} // setup basic req values
req.baseUrl = parentUrl;
req.originalUrl = req.originalUrl || req.url;
// next()...
}

  函式在最開始還是整理引數,這裡的restore沒看懂具體作用,暫時跳過這裡。

  總結來說第一部分做了以下事情:

1、獲取協議+基本地址的字串

2、獲取stack陣列,裡面裝的是layer物件

3、定義標記變數

4、對OPTIONS請求做特殊處理

5、done方法是所有layer跑完後的最終回撥,此時需要還原url

  對於OPTIONS方式的請求,若沒有做特殊處理,則會返回一個預設的響應。而在servlet中,則有一個特殊的doOptions的方法專門來設定Allow請求頭響應,感覺差不多。

  接下來呼叫一個next方法,該方法會被掛載到req上面,這是第一次呼叫:

proto.handle = function handle(req, res, out) {
// ...
next();
function next(err) {
// next('route')不會被當成錯誤
var layerError = err === 'route' ?
null :
err; // 去掉斜槓
if (slashAdded) {
req.url = req.url.substr(1);
slashAdded = false;
} // 還原被更改的req.url
if (removed.length !== 0) {
req.baseUrl = parentUrl;
req.url = protohost + removed + req.url.substr(protohost.length);
removed = '';
} // 退出路由的訊號
if (layerError === 'router') {
setImmediate(done, null)
return
} // 所有的layer都遍歷完畢
if (idx >= stack.length) {
setImmediate(done, layerError);
return;
} // 獲取請求的pathname
var path = getPathname(req); if (path == null) {
return done(layerError);
} // 尋找下一個匹配的layer
var layer;
var match;
var route; // ...more code
}
// ...
}

  這一部分主要做了下列事情:

1、判斷是否有err定義layerError變數,其中next('route')會被忽略

2、根據slashAdded變數決定是否需要切割一下url,還原完整的url(二級路由匹配)

3、除了route,router字串似乎在next中也有特殊意義?

  下面開始真正的匹配layer,如下:

while (match !== true && idx < stack.length) {
// 取出一個layer
layer = stack[idx++];
// 檢測layer是否匹配該路徑
match = matchLayer(layer, path);
route = layer.route; // ...
}

  這裡涉及到了Layer物件的原型方法,matchLayer(layer, path)實際上就是layer.match(path)。

  以假設條件看一下match的匹配過程:

// app.use('/',indexRouter)滿足fast_slash條件
Layer.prototype.match = function match(path) {
var match if (path != null) {
// layer匹配路徑為/時 匹配所有
if (this.regexp.fast_slash) {
this.params = {}
this.path = ''
return true
} // layer匹配路徑為*時 匹配所有:param
// 呼叫decodeURIComponent轉義path
if (this.regexp.fast_star) {
this.params = { '0': decode_param(path) }
this.path = path
return true
} // 用生成的正則解析
match = this.regexp.exec(path)
}
// 路徑不匹配 返回false
if (!match) {
this.params = undefined;
this.path = undefined;
return false;
} // 其餘情況下匹配的路徑
// 後面討論... return true;
}

  由於假設請求路徑為'/',所以這裡會跳過match階段,直接返回true。

  繼續看程式碼:

while (match !== true && idx < stack.length) {
// 取出一個layer
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;
// 報錯
if (typeof match !== 'boolean') layerError = layerError || match;
// Layer未匹配
if (match !== true) continue;
// app內部router物件的layer不存在route
if (!route) continue;
// 處理錯誤
if (layerError) {
// routes do not match with a pending error
match = false;
continue;
} // ...處理外部router物件上的layer
}

  需要注意的是,這裡的匹配是對app的內部路由上的Layer進行遍歷,而這些layer是沒有route物件掛載的,僅僅是用來分發外部路由,因此這裡會continue直接跳過後面的流程。

  由於已經匹配到對應的Layer,所以while迴圈跳出,繼續下面的流程:

// 根據配置引數處理引數合併
req.params = self.mergeParams ?
mergeParams(layer.params, parentParams) :
layer.params;
// 獲取layer匹配的path => ''
var layerPath = layer.path; // this should be done for the layer
self.process_params(layer, paramcalled, req, res, function(err) {
// ...trim_prefix(layer, layerError, layerPath, path)
});

  在生成路由會有一個合併引數的選項,決定是否將父路由的引數合併到子路由,預設為false。

  接下來獲取layer匹配的path後,呼叫了另外一個方法,而這個方法主要是處理/path:prarms這種形式的引數,所以跳過。

  而回調的trim_prefix函式內容也直接跳過,後面講,直接進入layer.handle_request函式:

Layer.prototype.handle_request = function handle(req, res, next) {
// 這裡的handle是router函式物件
var fn = this.handle;
// 錯誤處理中介軟體有4個引數
if (fn.length > 3) {
return next();
}
// 呼叫具體外部路由的handle方法
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};

  這裡從app的內部路由handle方法跳到了外部路由的handle中,再走一遍流程。

  由於req、res始終是一個,所以大部分的都可以跳過,這裡挑不同的地方來講:

1、stack

  var stack = self.stack;

  由於換了router,所以stack也換成了外部路由的stack,裡面裝的是有route掛載的layer。

2、while迴圈的後半段

// 獲取請求的方式
var method = req.method;
var has_method = route._handles_method(method); // OPTIONS請求特殊處理
if (!has_method && method === 'OPTIONS') {
appendMethods(options, route._options());
} // 如果route未處理該方式請求 直接跳過
if (!has_method && method !== 'HEAD') {
match = false;
continue;
} Route.prototype._handles_method = function _handles_method(method) {
// router.all
if (this.methods._all) {
return true;
}
var name = method.toLowerCase();
// head預設視為get請求
if (name === 'head' && !this.methods['head']) {
name = 'get';
}
// 判斷route是否有處理該請求方式的中介軟體
return Boolean(this.methods[name]);
}; // route[METHODS]
var layer = Layer('/', {}, handle);
layer.method = method; this.methods[method] = true;

  這裡做了一個提前判斷,在呼叫app[METHODS]、router[METHODS]時,最後指向底層的route[METHODS]。除了生成一個layer物件,還會同時將route的本地屬性methods物件上對應方式的鍵設為true,表示這個route有處理對應請求方式的layer。

  在跳過process_params、trim_prefix後,還是回到了handle_request方法。

  然而,這裡的layer對應的handle並不指向中介軟體函式,而是route.dispatch.bind(route),如下:

// router.get('/',fn1,fn2)...
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));

  真正的中介軟體函式是在layer.route上,所以這個是另外一個分發方法,負責把對應方式的請求轉給對應的route。

Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;
if (stack.length === 0) {
return done();
}
// 格式化請求方式
var method = req.method.toLowerCase();
if (method === 'head' && !this.methods['head']) {
method = 'get';
}
// 最終匹配的route
req.route = this; next(); function next(err) {
// signal to exit route
if (err && err === 'route') return done(); // err...
// 依次取出route物件stack中的layer
var layer = stack[idx++];
// err... if (err) {
layer.handle_error(err, req, res, next);
} else {
// 又是這個方法
layer.handle_request(req, res, next);
}
}
};

  這個dispatch與handle方法十分類似,依次取出layer並再次呼叫其handle_request方法,這裡的layer裡面的handle是最終處理響應請求的中介軟體函式。

  在文件中指出,需要執行中介軟體的第三個引數next中介軟體才會繼續走下去,從這裡也能看出,呼叫next後回到dispatch方法,會從stack上取出下一個layer,然後繼續執行中介軟體函式,直到所有的layer都過了一遍,會呼叫回撥函式done,這個方法就是最初router.handle裡面的next函式,開始下一輪讀取。

  當內部路由上的layer都過完,請求就處理完畢。正常情況下,會結束響應。接下來會呼叫最終回撥,簡單看一下比較複雜,後面單獨講。

  完結。