1.源碼目錄結構
Express4.x自己實現了一個router組件,實現http請求的順序流程處理,去除了很多綁定的中間件,使代碼更清晰。
下面是express4.14的文件結構圖:
1.middleware(中間件)下主要有init.js和query.js,init.js的作用是初始化request,response,而query.js中間件的作用是格式化url,將url中的rquest參數剝離,儲存到req.query中;
2.router文件夾為router組件,包括index.js、route.js和layer.js,router組件負責中間件的插入和鏈式執行,具體將在下面講解;
3.express.js和application.js是主要的框架文件,暴露了express的api;
4.request.js和response.js提供了一些方法豐富request和response實例的功能,如req.is、req.get、req.params、req.originalUrl等;
5.view.js封裝了模板渲染引擎,通過res.render()調用引擎渲染網頁。
2. Express啟動過程分析
先看一下官方示例
var express = require('express');
var app = express();
app.get('/', function(req, res){
res.send('Hello World');
});
app.listen(3000);
運行后訪問localhost:3000顯示Hello World。下面讓我們仔細看一下這段代碼。
首先第一行
var express = require('express');
require('express')載入了express框架,我們來看<font color=red size=3>源代碼中的index.js</font>
'use strict';
module.exports = require('./lib/express');
好吧,還要繼續require,我們來看<font color=red size=3>./lib/express.js</font>
'use strict';
/**
* 依賴模塊.
*/
var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');
/**
* 暴露`createApplication()`.
*/
exports = module.exports = createApplication;
/**
* Create an express application.
*
* @return {Function}
* @api public
*/
function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };
app.init();
return app;
}
從 <font color=red size=3>exports = module.exports = createApplication;</font>可以看出var express =require('express')最后實際是這個createApplication函數,createApplication就相當于express的'main'函數。
createApplication的開始定義了一個函數,函數有形參req,res,next為回調函數。函數體只有一條語句,執行<font color=red size=3> app.handle </font>,<font color=red size=3> handle </font>方法在<font color=red size=3> application.js </font>文件中定義,<font color=red size=3> handle </font>的代碼如下:
/**
* Dispatch a req, res pair into the application. Starts pipeline processing.
*
* If no callback is provided, then default error handlers will respond
* in the event of an error bubbling through the stack.
*
* @private
*/
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)
});
// no routes
if (!router) {
debug('no routes defined on app');
done();
return;
}
router.handle(req, res, done);
};
看它的注釋可知<font color=red size=3>app.handle</font>的作用就是將每對[req,res]進行逐級分發,作用在每個定義好的路由及中間件上,直到最后完成。
再看看createApplication方法中間的兩行
mixin(app, EventEmitter.prototype, false);
mixin(app, proto, false);
<font color=red size=3>mixin</font>是在頭部require載入的<font color=red size=3>merge-descriptors</font>模塊,它的代碼如下
function merge(dest, src, redefine) {
if (!dest) {
throw new TypeError('argument dest is required')
}
if (!src) {
throw new TypeError('argument src is required')
}
if (redefine === undefined) {
// Default to true
redefine = true
}
Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {
if (!redefine && hasOwnProperty.call(dest, name)) {
// Skip desriptor
return
}
// Copy descriptor
var descriptor = Object.getOwnPropertyDescriptor(src, name)
Object.defineProperty(dest, name, descriptor)
})
return dest
}
<font color=red size=3>Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {})</font>將src參數的屬性遍歷,屬性名稱傳入參數name中,<font color=red size=3>Object.defineProperty(dest, name, descriptor)</font> 則將src的每一個屬性name和name的值descriptor復制到目標參數dest中,所以 <font color=red size=3>mixin(app, proto, false);</font> 的作用即是將proto中所有的property全部導入進app,第三個參數false表示app中已有的屬性不被proto的屬性所覆蓋,proto定義了大部分express的public api,如app.set,app.get,app.use...詳見官方的API文檔。<font color=red size=3> mixin(app, EventEmitter.prototype, false); </font>則將Node.js的EventEmitter中的原型方法全部導入了app。
再來看createApplication接下來的兩行
app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };
這里定義了app的 <font color=red size=3>request</font> 和 <font color=red size=3>response</font> 對象,使其分別繼承自req(頂部導入的<font color=red size=3> request.js</font> )和res(頂部導入的<font color=red size=3> response.js</font> ),另外,把app對象賦值給app參數是為了后面在<font color=red size=3> request </font>和<font color=red size=3> response </font>對象中能夠通過<font color=red size=3> this.app </font>獲得所創建的<font color=red size=3>express實例</font>。
接下來是
app.init();。
顯然,作用是初始化,做哪些工作呢?
app.init = function(){
this.cache = {};
this.settings = {};
this.engines = {};
this.defaultConfiguration();
};
設定了cache對象(render的時候用到),各種setting的存儲對象,engines對象(模板引擎),最后進行默認的配置,代碼有點長這里就不貼了,就是做一些默認的配置。
看官網示例下一句
app.get('/', function(req, res){
res.send('Hello World');
});
app.get可以獲取app.set設置的全局變量,也可以設置路由的處理函數,下面是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;
};
});
methods是一個數組,存儲了http所有請求的類型,在method模塊里定義,除了基本的get、post請求外,還有多達十幾種請求,可能是為了兼容新的http標準吧。app[method]中,method=='get'且只有一個參數,則執行set,將route和回調存儲進一個棧中。遇到http請求時觸發執行,app.get也將產生一條路由中間件,執行后返回瀏覽器html頁面。具體的路由組件代碼將在后面分析。
還有最后一句
app.listen(3000);
listen方法的代碼如下
app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
這里其實是調用了Node.js原生的http模塊的CreatServer方法創建服務器。
3.中間件
所謂中間件,就是在收到請求后和發送響應之前這個階段執行的一些函數。
express對象的use方法可以在一條路由的處理鏈上插入中間件,如
//加載路由
app.use( '/', require( './routes/index' ) );
當你為某個路徑安裝了中間件,則當以該路徑為基礎的路徑被訪問時,都會應用該中間件。比如你為“/”設置了中間件,那么所有請求都會應用該中間件。
中間件函數的原型如下:
function (req, res, next)
第一個參數是Request對象req。第二個參數是Response對象res。第三個則是用來驅動中間件調用鏈的函數next,如果你想讓后面的中間件繼續處理請求,就需要調用next方法。
app.static中間件
Express提供了一個static中間件,可以用來處理網站里的靜態文件的GET請求,可以通過express.static訪問。express.static的用法如下:
express.static(root, [options])
第一個參數root,是要處理的靜態資源的根目錄,可以是絕對路徑,也可以是相對路徑。第二個可選參數用來指定一些選項,比如maxAge、lastModified等,一個典型的express.static應用如下:
//全局變量
global.ABSPATH = path.join( __dirname, '/' );
//express應用static中間件
const staticOptions = {
dotfiles: "ignore", //allow,deny,ignore
etag: true,
extensions: false,
index: "index.html", //set false to disable directory indexing
lastModified: true,
maxAge: 0,
redirect: true,
setHeaders () {}
};
app.use( express.static( path.join( __dirname, 'public' ), staticOptions ) );
app.use( express.static( path.join( __dirname, 'upload' ), staticOptions ) );
上面這段代碼將相對路徑下的public和upload目錄作為靜態文件,并設置staticOptions 那些屬性,如Cache-Control頭部的max-age選項為0天。還有其它一些屬性,請對照express.static的文檔來理解。
4.Router組件
Router是Express中一個非常核心的東西,基本上就是一個簡化版的Express框架。下面我們來一起看看,app.get()是如何實現的,之前我們在application.js中已經發現app.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;
};
});
從上面的代碼可以看出app.get()函數如果參數長度是1,則返回app.set()定義的變量,如果參數長度大于1,則進行路由處理。繼續往下看 this.lazyrouter(),從名字來看,好像是懶加載router,那我們看看源碼:
app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router({
caseSensitive: this.enabled('case sensitive routing'),
strict: this.enabled('strict routing')
});
this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));
}
};
由此可以看出_router是Router的實例,如果_router不存在,就new一個Router出來,而這個Router就是我們剛才在目錄結構中看到的router目錄,也就是今天的主角Router組件。繼續上邊的代碼,加載完_router之后,執行了this._router.route(path)這樣一行代碼,那這行代碼有做了什么呢,我們再繼續往下挖,我們在router目錄下的index.js中找到了它的實現:
//Create a new Route for the given path.
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;
};
這里new了一個Route對象,并且new了一個Layer對象,然后將Route對象賦值給layer.route,最后將這個Layer添加到stack數組中。我們先來看看Route,這個Route是什么呢,它和Router組件有什么關系呢?
這里先聲明一下,本文提到的路由容器(Router)代表“router/index.js”文件的到導出對象,路由中間件(Route)代表“router/route.js”文件的導出對象。
首先,Router是怎么來的呢,Router對象只會在首次調用lazyrouter時被實例化,然后賦值給app._router字段。而Route只是路由中間件,封裝了路由信息,這里要特別注意<font color=red size=3> Router與Route的區別,Router可以看作是一個中間件容器,不僅可以存放路由中間件(Route),還可以存放其他中間件,在lazyrouter方法中實例化Router后會首先添加兩個中間件:query和init;而Route 僅僅是路由中間件,封裝了路由信息。Router和Route都各自維護了一個stack數組,該數組就是用來存放中間件和路由的。</font>
Router和Route的stack是有差別的,這個差別主要體現在存放的layer(<font color=red size=3>layer是用來封裝中間件的一個數據結構</font>)不太一樣
由于Router.stack中存放的中間件包括但不限于路由中間件,而只有路由中間件的執行才會依賴與請求method,因此Router.stack里的layer沒有method屬性,而是將其動態添加到了Route.stack的layer中;layer.route字段也是動態添加的,可以通過該字段來判斷中間件是否是路由中間件??梢酝ㄟ^兩種方式添加中間件:app.use和app[method],前者用來添加非路由中間件,后者添加路由中間件,這兩種添加方式都在內部調用了Router的相關方法來實現:
/*在router目錄下的index.js*/
/*添加非路由中間件*/
proto.use = function use(fn) {
var offset = 0;
var path = '/';
/* 此處略去部分代碼 */
callbacks.forEach(function (fn) {
if (typeof fn !== 'function') {
throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
}
// 添加中間件
debug('use %s %s', path, fn.name || '<anonymous>');
//實例化layer對象并進行初始化
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
}
return this;
/*添加路由中間件*/
proto.route = function(path){
//實例化路由對象
var route = new Route(path);
//實例化layer對象并進行初始化
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));
//指向剛實例化的路由對象(非常重要),通過該字段將Router和Route關聯來起來
layer.route = route;
this.stack.push(layer);
return route;
};
對于路由中間件,路由容器中的stack(<font color=red size=3>Router.stack</font>)里面的layer通過route字段指向了路由對象,那么這樣一來,<font color=red size=3>Router.stack</font>就和<font color=red size=3>Route.stack</font>發生了關聯,關聯后的示意模型如下圖所示:
這里大家就會發現,express實例在處理路由的時候,會先創建一個Router對象,然后用Router對象和對應的path來生成一個Route對象,最后由Route對象來處理具體的路由實現。
好了,那接下來我們繼續深入研究,看看route.method究竟做了什么,我們找到route.js文件,發現如下的代碼:
methods.forEach(function(method){
Route.prototype[method] = function(){
var handles = flatten(slice.call(arguments));
for (var i = 0; i < handles.length; i++) {
var handle = handles[i];
if (typeof handle !== 'function') {
var type = toString.call(handle);
var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
throw new Error(msg);
}
debug('%s %s', method, this.path);
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
}
return this;
};
});
原來route和application運用了同樣的技巧,通過循環methods來動態添加method函數,我們直接看函數內部實現,首先通過入參獲取到handles,這里的handles就是我們定義的路由中間件函數,這里我們可以看到是一個數組,所以我們可以給一個路由添加多個中間件函數。接下來循環handles,在每個循環中利用handle來創建一個Layer對象,然后將Layer對象push到stack中去,這個stack其實是Route內部維護的一個數組,用來存放所有的Layer對象?,F在你一定想這道這個Layer到底是什么東西,那我們來看看layer.js的源代碼:
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %s', path);
var opts = options || {};
this.handle = fn;
this.name = fn.name || '<anonymous>';
this.params = undefined;
this.path = undefined;
this.regexp = pathRegexp(path, this.keys = [], opts);
if (path === '/' && opts.end === false) {
this.regexp.fast_slash = true;
}
}
上邊是Layer的構造函數,我們可以看到這里定義handle,params,path和regexp等幾個主要的屬性:
1.其中最重要的就是handle,它就是我們剛剛在route中創建Layer對象傳入的中間件函數。
2.params其實就是req.params,至于如何實現的我們可以以后再做探討,今天先不做說明。
3.path就是我們定義路由時傳入的path。
4.regexp對于Layer來說是比較重要的一個屬性,因為下邊進行路由匹配的時候就是靠它來搞定的,而它的值是由pathRegexp得來的,其實這個pathRegexp對應的是一個第三方模塊path-to-regexp,它的功能是將path轉換成regexp。
我們再來看看Layer有什么方法:
/**
* Check if this route matches `path`, if so
* populate `.params`.
*
* @param {String} path
* @return {Boolean}
* @api private
*/
Layer.prototype.match = function match(path) {
if (path == null) {
// no path, nothing matches
this.params = undefined;
this.path = undefined;
return false;
}
if (this.regexp.fast_slash) {
// fast path non-ending match for / (everything matches)
this.params = {};
this.path = '';
return true;
}
var m = this.regexp.exec(path);
if (!m) {
this.params = undefined;
this.path = undefined;
return false;
}
// store values
this.params = {};
this.path = m[0];
var keys = this.keys;
var params = this.params;
for (var i = 1; i < m.length; i++) {
var key = keys[i - 1];
var prop = key.name;
var val = decode_param(m[i]);
if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
params[prop] = val;
}
}
return true;
};
match函數主要用來匹配path的,當我們向express發送一個http請求時,當前請求對應的是哪個路由,就是通過這個match函數來判斷的,如果path中帶有參數,match還會把參數提取出來賦值給params,所以說match是整個路由中很重要的一點。還有下面一個處理中間件的函數
/**
* Handle the request for the layer.
*
* @param {Request} req
* @param {Response} res
* @param {function} next
* @api private
*/
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
if (fn.length > 3) {
// not a standard request handler
return next();
}
try {
fn(req, res, next);
} catch (err) {
next(err);
}
};
從上邊的代碼我們可以看到調用了fn,而這個fn就是layer的handle屬性,就是我們定義路由時傳入的路由中間件,到這里我們總算找到了我們的路由中間件被執行的地方。那Layer和Route之間又有什么千絲萬縷的聯系呢?
每個Route都會維護一個Layer數組,每一個Layer對應一個中間件函數,Layer存儲了每個路由的path和handle等信息,并且實現了match和handle的功能,所以可以發現Route和Layer是一對多的關系,每個Route代表一個路由,而每個Layer對應的是路由的每一個中間件函數。
講完了Route和Layer的關系,我們再來回頭看看Router和Layer的關系,從index.js中prop.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;
};
我們可以看出來Router每次添加一個route,都會把route包裝到layer中,并且將layer添加到自己的stack中,那為什么要把route包裝到layer中呢,前邊我們已經仔細研究了Layer模塊的代碼,我們發現Layer具有match和handle的功能,這樣我們就可以通過Layer的match來進行route的匹配了。這里有一個關鍵點我們需要特別講解下,上邊的代碼中在創建Layer對象的時候傳入的handle函數為route.dispatch.bind(route),route.dispatch是通過next()獲取stack中的每一個layer來執行相應的路由中間件,這樣就保證了我們定義在路由上的多個中間件函數被按照定義的順序依次執行。
我們接下來來重新梳理一下router相關的所有內容,看看express究竟是如何對http請求進行路由的。
當客戶端發送一個http請求后,會先進入express實例對象對應的router.handle函數中,router.handle函數會通過next()遍歷stack中的每一個layer進行match,如果match返回true,則獲取layer.route,執行route.dispatch函數,route.dispatch同樣是通過next()遍歷stack中的每一個layer,然后執行layer.handle_request,也就是調用中間件函數。直到所有的中間件函數被執行完畢,整個路由處理結束。
5.View的實現
渲染模板使用的是 res.render(),它實現總體來說經過三次封裝,進行了一些配置,調用鏈條為
res.render() => app.render() =>view.render()=> require("jade")/reqiure("ejs").render()
。
首先看app.engine,將jade或ejs模板引擎的render函數存入了engines數組中
app.engine = function engine(ext, fn) {
if (typeof fn !== 'function') {
throw new Error('callback function required');
}
// get file extension
var extension = ext[0] !== '.'
? '.' + ext
: ext;
// store engine
this.engines[extension] = fn;
return this;
};
app.defaultConfiguration()(application.js中初始化的一個函數),把View的構造函數保存。
// default configuration
this.set('view', View);
app.render()將其取出并調用,初始化一個View實例,并執行‘view.render()’渲染模板,注意初始化函數將engines傳入了View實例,里面保存了模板引擎的render函數。
app.render = function render(name, options, callback) {
var cache = this.cache;
var done = callback;
var engines = this.engines;
var opts = options;
var renderOptions = {};
var view;
/*此處省略部分代碼*/
// view
if (!view) {
var View = this.get('view');
view = new View(name, {
defaultEngine: this.get('view engine'),
root: this.get('views'),
engines: engines
});
if (!view.path) {
var dirs = Array.isArray(view.root) && view.root.length > 1
? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
: 'directory "' + view.root + '"'
var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
err.view = view;
return done(err);
}
// prime the cache
if (renderOptions.cache) {
cache[name] = view;
}
}
// render
tryRender(view, renderOptions, done);
};
view.render()執行的便是模板引擎的render函數,callback為渲染完成后的回調函數。
View.prototype.render = function render(options, callback) {
debug('render "%s"', this.path);
this.engine(this.path, options, callback);
};
總結視圖渲染的流程為, res.render() 調用了 app.render() 。在 app.render() 中,先創建一個 view 對象(相關源碼為 view.js ),然后調用 view.render() 。如果允許緩存,即 app.enabled('view cache') 的話,則會優先檢查緩存,如果緩存中已有相關視圖,則直接取出;否則才會新創建一個視圖對象。
最后總結一下,其實整個Express執行過程就是往req、res不停地添加和修改屬性;中間件也是通過app作為回調,進而修改req、res;其中app.use和app.static用來添加中間件,app.handle則將每對[req,res]進行逐級分發,作用在每個定義好的路由及中間件上,直至最后完成分發。