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对象。现在你一定想这道这个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]进行逐级分发,作用在每个定义好的路由及中间件上,直至最后完成分发。
网友评论