内容简介:NodeJS官方提供的最简单的服务器例子如下:Express框架没有那么神奇,只是代理了通过阅读源码,我觉得可以把Express逻辑分成两段:启动服务和响应请求。
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); }); }); 复制代码
以上所述就是小编给大家介绍的《Express源码解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。