概述
NodeJS官方提供的最简单的服务器例子如下:
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World!\n');
});
复制代码
Express框架没有那么神奇,只是代理了http.createServer(requestHandler)
中的requestHandler。并使用已经注册了的中间件和路由匹配响应传来的用户请求。
整体思路
通过阅读源码,我觉得可以把Express逻辑分成两段:启动服务和响应请求。
启动服务阶段指的是http.createServer(requestHandler)
和server.listener()
两个API被调用前执行的一系列初始化工作。
响应请求阶段指的是服务器接收来自客户端请求时触发的request事件的handler。
启动服务阶段
启动服务最重要的部分就是注册中间件和路由了。
中间件和路由可以说是几乎所有服务器都会提供的功能。在Express框架里,中间件和路由都会抽象成layer对象,在这篇文章里,存储中间件layer对象的容器叫做中间件router对象,存储路由layer对象的容器叫做路由router对象。
在Express框架里,中间件就是匹配路径就会执行的回调,而路由不仅要匹配路径还要匹配http method(如get、post之类)。所以对于中间件router对象,匹配路径之后会直接执行回调,但是路由router对象的匹配路径之后执行的回调统一为router.handle(req, res, next)
,里面的逻辑会继续匹配http method。
1. app.use
方法
不论是注册中间件router对象还是路由router对象,我们都会使用app.use
。
app.use
方法实质上是调用它自身的router对象的use方法:
var router = this._router;
fns.forEach(function (fn) {
// non-express app
if (!fn || !fn.handle || !fn.set) {
return router.use(path, fn);
}
debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
// mounted an app
fn.emit('mount', this);
}, this);
复制代码
2. 中间件router对象
当我们调用类似app.use('/', fn)
这样的语句,其实就是注册中间件。
这里必须说明一下,每一个express app初始化的时候会使用app.lazyrouter()
来实例化一个router对象,在这篇文章里,我们姑且叫它中间件router对象,因为它主要是负责储存中间件layer对象的,但是它还可以注册router对象,例如开发中我们会调用形如app.use('/test', testRouter)
的语句。
中间件router对象维护这一个stack数组,用来装载Layer对象。
当router对象的use方法被调用的时候,就会把路径和回调封装成一个Layer对象,并放入stack数组中。
请注意:中间件router对象的layer对象的route是undefined,跟路由router对象的layer对象的route是不一样的。
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
layer.route = undefined;
this.stack.push(layer);
复制代码
3. 路由router对象
当我们调用形如app.use('/test', testRouter)
的语句,可以表述为注册了一个路由中间件,而这个中间件就是下面的router
函数:
function router(req, res, next) {
router.handle(req, res, next);
}
复制代码
为了区别与中间件router对象,在这篇文章里,把注册在中间件router对象上的路由中间件定义为路由router对象。
到这里,我最想告诉大家的是,在express里,router对象是可以通过这种方式嵌套的。
就和前面提到的一样,路由也会被抽象成layer对象,并把router
函数作为Layer构造函数的第三个参数传入。
4. HTTP Method方法和Route实例
HTTP Method指的是get、post、put、delete、header之类的http请求方法。
路由router对象不仅需要匹配路径还需要匹配HTTP Method。而负责匹配HTTP Method的功能是由Route实例来完成。
当我们在调用app[method]
或者router[method]
时,就是在调用router.route
方法(就是下面的this.route(path)
),如下:
// create Router#VERB functions
methods.concat('all').forEach(function(method){
proto[method] = function(path){
var route = this.route(path)
route[method].apply(route, slice.call(arguments, 1));
return this;
};
});
复制代码
router.route
方法里面会生成一个新的layer对象,并把回调设置为route.dispatch.bind(route)
,这一点与前面提到的中间件router对象不同,而且layer的route不再是undefined,最后返回新的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实例的作用是什么呢?先看看它的构造函数:
function Route(path) {
this.path = path;
this.stack = [];
debug('new %o', path)
// route handlers for various http methods
this.methods = {};
}
复制代码
Route实例维护着一个stack数组,作用是收集Layer对象;还维护这一个methods对象,作用是指示该route对象可以匹配的http methods。
route收集的Layer对象维护着路由真正的回调,就是下面的handle:
var layer = Layer('/', {}, handle);
layer.method = method;
this.methods[method] = true;
this.stack.push(layer);
复制代码
5. Layer对象
一个Layer对象维护这一个路径和回调,它会把路径正则表达式化,用以在响应请求阶段匹配路径,先看看它的构造函数:
function Layer(path, options, fn) {
if (!(this instanceof Layer)) {
return new Layer(path, options, fn);
}
debug('new %o', 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);
// set fast path flags
this.regexp.fast_star = path === '*'
this.regexp.fast_slash = path === '/' && opts.end === false
}
复制代码
有三种layer对象:
Layer类别 | route | method |
---|---|---|
中间件Layer | undefined | undefined |
路由Layer | 非undefined | undefined |
route Layer | undefined | 非undefined |
中间件Layer实例的回调是fn,也就是注册的中间件函数;路由Layer实例的回调都是function router(req, res, next)
;route Layer实例的回调都是route.dispatch.bind(route)
。
响应请求阶段
通过启动服务阶段,我们已经把服务器的准备工作完成 —— 注册了中间件和路由。
当应用执行到server.listener()
时,就可以开始接受并处理客户端的请求,最后返回服务器响应。
1. 增强req对象和res对象
当一个请求到来的时候,NodeJS会把请求抽象成req(http.IncomingMessage的实例),把响应抽象成res(http.ServerResponse的实例),传入server的request事件的handler,但是在Express框架里,req对象和res对象被增强了。
增强内容可以参考express.js同目录下的request.js和response.js。
那么是怎么增强的呢?
在app.lazyrouter
方法里,已经添加了一个中间件,就是下面的middleware.init(this)
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));
}
};
复制代码
而在middleware.init(this)
里,可以看到重新设置了req和res的原型:
exports.init = function(app){
return function expressInit(req, res, next){
if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
req.res = res;
res.req = req;
req.next = next;
setPrototypeOf(req, app.request)
setPrototypeOf(res, app.response)
res.locals = res.locals || Object.create(null);
next();
};
};
复制代码
2. 正则表达式匹配中间件和路由
由于在启动服务阶段,我们已经注册好了中间件和路由,并把它们都抽象成layer对象,所以在处理请求阶段的时候,就清晰明了了。
基本逻辑是: 遍历router维护的stack容器; 对于中间件layer(就是layer.route为undefined的),路径匹配成功后就可以执行中间件函数了; 对于路由layer(就是layer.route不是undefined的),路径匹配成功后还需要匹配http method才能执行路由函数。
这一过程,有如下的重要方法:
app.handle,express app处理请求的入口,实质上是调用了自身router的handle router.handle,遍历router维护的stack数组,找到匹配路径的layer对象 Route.prototype._handles_method,对于路由layer对象,还需要这个方法验证是否可以匹配http method Route.prototype.dispatch,遍历route维护的stack数组,找到匹配路径和http method的layer对象 Layer.prototype.match,路径匹配的关键 Layer.prototype.handle_request,匹配成功后执行回调
3. 模板引擎
模板引擎并不是express作者原创的,而是引入了别的第三方库,然后使用第三方库提供的API渲染出响应页面,并返回给客户端。
目前支持较多的是ejs
和pug
这两个模板引擎。
Express镶嵌
一个Express app是可以挂载到另一个Express app上的,因为本质上一个Express app就是为了维护起自身的router对象,所以挂载的方式其实就是在parent express app的上注册一个中间件,该中间件负责把req和res传递给child express app,并让它们建立起父子关系,源码如下:
// restore .app property on req and res
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});
复制代码