剛剛接觸express,它的中間件確實把我搞得頭暈。get的回調中要不要加next?不加載還會執行下一個中間件么?給get指定'/'路徑是不是所有以'/'開頭的訪問在沒有確切匹配時都能執行?use件又有什么區別,use中不加next是不是也可以繼續執行下一個next?這些問題就是最困擾我的問題。為了搞清楚這些問題,我開始查看express的源碼。下面就以一次get方法分析express的加載與調用流程。
以下面的代碼為例,命名為test.js:
var express = require('express');
var app = express();
app.get('/',function myFunc(req, res) {
res.send('this is the Homepage');
});
app.listen('8080');
1.加載
var express = require('express');
導入了express目錄的express.js文件,文件中聲明的輸出為createApplication函數。
var app = express();
調用了上述createApplication函數,來看這個函數(部分):
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
app.init();
return app;
}
在這個函數中,初始化了一個app對象。可以看到:
- 這個對象是一個遵從http回調簽名的函數,后面可以看到,例程最后一行的代碼app.listen('8080')中,app.listen函數中就是將app最為一個回調函數初始化了一個http server。
- 在createApplication函數中,通過mixin(app, proto, false),將proto的屬性和方法傳給了app,而proto即通過require('./application')引用的同目錄下application.js文件。Mixin語句將application.js中為app實例聲明的屬性和方法傳遞給app。之后,createApplication函數又調用了app.init(),這個方法在application.js文件中聲明(在application.js中,聲明了一個app實例的大部分屬性和方法)。
例程test.js第三行調用了app.get方法。application.js對get方法的聲明:
methods.forEach(function(method){
app[method] = function(path){
if (method === 'get' && arguments.length === 1) {
// app.get(setting)
return this.set(path);
}
this.lazyrouter();
var route = this._router.route(path);
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
函數首先針對get方法只有一個參數時作出了定義,此時get方法返回app的設定屬性,跟我們的例程沒有關系。
this.lazyrouter()為app實例初始化了基礎router對象,並調用router.use方法為這個router添加了兩個基礎層,回調函數分別為query和middleware.init。我們不去管這個過程。
下一句var route = this._router.route(path)就以第一個參數path調用了router.route方法(router在lazyrouter初始化)。router在router目錄中index.js文件中聲明,它的屬性stack存儲了以layer描述的各個中間層。route方法定義在proto.route函數中,代碼如下:
proto.route = function route(path) {
var route = new Route(path);
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer);
return route;
};
可以看到,首先創建了一個新的route實例;然后將route.dispatch函數作為回調函數創建了一個新的layer實例,並將layer的route屬性設置為這個route實例之后,將這個layer推入router(this.stack的this是router)的stack中。
形象地說,這個過程就是新建了一個layer作為中間層放入了router的stack數組中。這個layer的回調為route.dispatch。
執行完這個router.route方法后,又通過route[method].apply(route, slice.call(arguments, 1));讓生成的這個route(不是router)調用了route.get。route.get中的關鍵流如下:
var handles = flatten(slice.call(arguments));//傳入的回調在route[method].apply(route, slice.call(arguments, 1));中明確,即用戶定義的回調函數myFunc
var layer = Layer('/', {}, handle);//以myFunc作為回調新建一個layer,設置method屬性
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);//這里的this是route對象,它也維護了一個stack(不是router的stack),存放了當前route對象的所有layer,每個layer包裝了一個回調函數。
到此,程序就完成了對get方法的加載。
我們簡短地回顧下這個過程:首先為app實例化一個router對象,這個對象的stack屬性是一個數組,保存了app的不同中間層。一個中間層以一個layer實例表征,這個layer的handle屬性引用了回調函數。對於get等方法創建的layer,它的handle為route.dispatch函數,而在get方法中自定義的回調函數是存放在route的stack中的。如果例程中繼續為app添加其他路由,則router對象會繼續生成新的layer存儲這些中間件,並放入自己的stack中。
2.調用
來看例程最后一句app.listen('8080')。listen方法如下:
app.listen = function listen() {
var server = http.createServer(this);//this是例程中的app
return server.listen.apply(server, arguments);
};
listen方法以this為參數生成了一個http server,this是例程中的app,就是以app為回調函數生成了一個http server。前面提到,app是一個遵從http回調簽名的函數,就是因為它就是在request發生時候的回調函數。
當server收到一個request時,調用app函數。app函數內只有一個語句:
app.handle(req, res,next);
handle函數在application.js中,代碼(部分):
app.handle = function handle(req, res, callback) {
var router = this._router;
// final handler
var done = callback || finalhandler(req, res, {
env: this.get('env'),
onerror: logerror.bind(this)
});
router.handle(req, res, done);
};
首先獲取了app的router對象,然后調用router.handle方法。此時callback繼承express.js中調用時的next,為undifined,所以done就為finalhandler函數,這個函數在服務器結束響應時調用。
繼續看router.handle,這個函數的關鍵是閉包next()函數,此時先執行了next一次。進入while (match !== true && idx < stack.length)循環。通過debug可以知道,此時stack(是router的stack) 有3層,next函數內部:
layer = stack[idx++];
match = matchLayer(layer, path);
//('path matches layer?'+match);
route = layer.route;
先取出第一層,判斷與request的path是否match。第一、二層是router初始化時的query函數和middleware.init函數,它們都會進入執行trim_prefix(layer, layerError, layerPath, path);的分支,並調用其中的layer.handle_request(req,res, next);,這個next就是router.handle函數里的閉包next。執行了這兩層后,繼續回調next函數。
這時就執行到了加載時生成的route所在的層,判斷request路徑是否匹配,這里的匹配執行的是嚴格匹配,比如這層的regexp屬性(從加載時的路由確定)是'/',那么'/a'也不能匹配。
若路徑不匹配,while循環會直接跳過當此循環,對router.stack的下一層進行匹配;如果path與這個route的regexp匹配,就會執行layer.handle_request(req, res, next);。
layer.handle_request函數:
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
執行這層的回調函數fn=this.handle,我們在加載時分析過,這層的回調函數是route.dispatch函數,這個函數用來處理route實例內的路由選擇。來看這個函數(部分):
Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack;//this是route,它的stack中存儲的用戶定義的函數myFunc
var method = req.method.toLowerCase();
req.route = this;
next();
function next(err) {
var layer = stack[idx++];//第一次調用時獲得用戶定義的函數myFunc所在層
if (!layer) {
return done(err);
}
if (err) {
layer.handle_error(err, req, res, next);
} else {
layer.handle_request(req, res, next);// myFunc所在層調用handle_request
}
}
};
到layer.handle_request(req, res, next);時,myFunc所在層調用handle_request。
var fn = this.handle;//fn=myFunc
如果myFunc中沒有引用next,它執行完后就回到了layer.handle_request(req, res, next);中。這樣layer.handle_request(req, res, next);執行結束,回到調用它的route的next函數中,這個next運行結束,dispatch函數也運行結束。由於沒有引用done簽名參數,done所引用的router->next也不再運行。這樣router->layer[dispatch]層的handle_request函數也執行完畢。router->next也執行完畢,從而依次執行完router初始化的兩層[query]和[middleware.init]后,router.handle也執行完畢。整個流程處理結束。
如果myFunc中引用了next,則route.dispatch->next會再次被調用,如果這個route只有這一個handle函數,則在運行到
var layer = stack[idx++];
if (!layer) {
return done(err);
}
時會返回done(err)。這里的done函數是從layer.handle_request中傳遞來的router->next,於是調用router.handle->next(err);這樣就實現了對router.stack中的下一個中間件的調用。
從上面的分析可以看出,express在處理中間層時,主要用了router、layer、route三個類。一個app實例有一個基礎的router,用來處理所有的中間件;針對每個get等方法請求,都會實例化一個新的route對象。router和route實例都會維護自己的stack數組屬性,以存放其路由信息。stack的每個元素都是一個layer,layer對中間件的回調函數進行了包裝。
而且看到,循環進入下一個中間件的next函數,都是定義在router和route中,而調用一個中間件后再進入下一層則是通過layer實例的接口實現。
3.篇頭的問題
get的回調中要不要加next,不加載next還會執行下一個中間件么?
如果這個方法回調后不需要再執行其他中間件,不需要引用next。但不添加next並不影響其它不同路由的執行,若當前的get方法不匹配請求路徑,router會繼續向下尋找;匹配路徑后,執行完當前回調就不再尋找。
給get指定’/’路徑是不是所有以‘/’開頭的訪問在沒有確切匹配時都能執行?
不行。Router在執行匹配時是嚴格匹配,如果只有’/’,’/a’是不能匹配的。要想所有的請求匹配,可以指定’/*’為最后一個路徑,這樣所有查找不到的請求會被這個回調相應。
use件又有什么區別,use中不加next是不是也可以繼續執行下一個next?
在next的使用上,use與get等方法是一致的。use方法不添加next,則執行完use的回調后不會再執行其他中間件。
use在執行匹配的時候與get等方法是有區別的。例如app.use('/'){}中,請求地址為/aaa也可以訪問到;但如果是app.get('/'),請求中在/之后再加其他字母就無法訪問。
轉載一篇express源碼分析的文章,寫得很好:
從express源碼中探析其路由機制
后記:其實仔細看了express的API,很多問題就清楚了...