express源码阅读记 -- 当一个hello worle 渲染到前端界面的时候express做了什么

前几天有空瞄了几眼express4.x的源码,今天做一下总结。 首先我会使用以下代码做为一个入口,开始

  1. const express = require('express');
  2. const app = express();
  3. app.get('/',indexHandler)
  4. function indexHandler(req,res,next){
  5. res.set('Content-Type',"text/html;charset=utf-8");
  6. res.send(`<h1 style="color:red">hello world</h1>`)
  7. }
  8. app.listen(9999)

从第5行代码开始看起,先翻到源码:

  1. function createApplication() {
  2. var app = function(req, res, next) {
  3. app.handle(req, res, next);
  4. };
  5. mixin(app, EventEmitter.prototype, false);
  6. mixin(app, proto, false);
  7. // expose the prototype that will get set on requests
  8. app.request = Object.create(req, {
  9. app: { configurable: true, enumerable: true, writable: true, value: app }
  10. })
  11. // expose the prototype that will get set on responses
  12. app.response = Object.create(res, {
  13. app: { configurable: true, enumerable: true, writable: true, value: app }
  14. })
  15. app.init();
  16. return app;
  17. }

首先注意一下这个叫做app的函数,他既是我们本段代码的入口,也是http请求过来时要流过的第一个函数,然后下来的两个minxin,第一个是把EventEmitter.prototype中的所有属性合并到app上来,这样一来app就拥有了事件订阅和提交的功能,关于EventEmitter可以查看node官网的文档详情。 第二个minxin 是将一个叫做proto的东西合并到了app上,这个proto是在application.js文件里边,定义了app上的方法,诸如listen,enabled,disabled,set等等,这些方法可以在express官方文档这里看到
下来,是定义了request,response,并分别在其上面用app属性引用了app,然后调用app.init()方法。

  1. app.init = function init() {
  2. this.cache = {};
  3. this.engines = {};
  4. this.settings = {};
  5. this.defaultConfiguration();
  6. };

chache是于保存render时的结果的,engines是模板引擎,至于settings保存的是一些设置,诸如是否开启e-tag,x-powerd,还有响应头的一些字段。下来调用了defaultConfiguration。代码比较长,直接在源码里做注释了:

  1. app.defaultConfiguration = function defaultConfiguration() {
  2. var env = process.env.NODE_ENV || 'development';
  3. // default settings
  4. /*添加头x-powered-by*/
  5. this.enable('x-powered-by');
  6. /* 设置etag
  7. > tips:ETag有两种类型:强ETag(strong ETag)与弱ETag(weak ETag)。
  8. 强ETag表示形式:"22FAA065-2664-4197-9C5E-C92EA03D0A16"。
  9. 弱ETag表现形式:w/"22FAA065-2664-4197-9C5E-C92EA03D0A16"。
  10. 具体的策略得看浏览器的不同实现
  11. */
  12. this.set('etag', 'weak');
  13. /*设置环境变量,开发还是生产*/
  14. this.set('env', env);
  15. /* query解析函数,extendend策略下最终调用的是:qs.parse(str, {allowPrototypes: true});*/
  16. this.set('query parser', 'extended');
  17. /*访问req.subdomains时host用.分割成数组之后需要删除后边的数目是几个,举例: 默认为2,tobi.ferrets.example.com 的subdomains 就是["ferrets", "tobi"]
  18. */
  19. this.set('subdomain offset', 2);
  20. /*是否信任代理,*/
  21. this.set('trust proxy', false);
  22. // trust proxy inherit back-compat
  23. Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
  24. configurable: true,
  25. value: true
  26. });
  27. debug('booting in %s mode', env);
  28. /* 当一个子app 挂载到父app的时候会触发 */
  29. this.on('mount', function onmount(parent) {
  30. // inherit trust proxy
  31. if (this.settings[trustProxyDefaultSymbol] === true
  32. && typeof parent.settings['trust proxy fn'] === 'function') {
  33. delete this.settings['trust proxy'];
  34. delete this.settings['trust proxy fn'];
  35. }
  36. // inherit protos
  37. setPrototypeOf(this.request, parent.request)
  38. setPrototypeOf(this.response, parent.response)
  39. setPrototypeOf(this.engines, parent.engines)
  40. setPrototypeOf(this.settings, parent.settings)
  41. });
  42. // setup locals
  43. this.locals = Object.create(null);
  44. // top-most app is mounted at /
  45. this.mountpath = '/';
  46. // default locals
  47. this.locals.settings = this.settings;
  48. // default configuration
  49. /* view为render的时候渲染模板的一个数据结构 */
  50. this.set('view', View);
  51. /* 模板目录,默认为views */
  52. this.set('views', resolve('views'));
  53. /* 设置jsonp 回调函数的名字*/
  54. this.set('jsonp callback name', 'callback');
  55. /*生产模式开启view缓存*/
  56. if (env === 'production') {
  57. this.enable('view cache');
  58. }
  59. /* 4.x不再支持app.router式的调用 */
  60. Object.defineProperty(this, 'router', {
  61. get: function() {
  62. throw new Error('\'app.router\' is deprecated!\nPlease see the 3.x to 4.x migration guide for details on how to update your app.');
  63. }
  64. });
  65. };

至此,app.init行完毕。 然后express函数返回app实例。
接着,我们使用app.get定义了我们的第一条路由,至于app.get的源码,是在这里:

  1. // methods = ['get','post'...] 等一系列http动词
  2. methods.forEach(function(method){
  3. app[method] = function(path){
  4. if (method === 'get' && arguments.length === 1) {
  5. // app.get(setting)
  6. return this.set(path);
  7. }
  8. this.lazyrouter();
  9. var route = this._router.route(path);
  10. route[method].apply(route, slice.call(arguments, 1));
  11. return this;
  12. };
  13. });

app[method]向外引用,function,闭包了当前method的名字,这里注意到当以app.get(‘key’)形式调用的时候,程序实际return的是当前set[‘key’]的值。
如果是定义路由,则走了下面的步骤,首先会给当前的app实例初始化一个router实例,源码如下:

  1. app.lazyrouter = function lazyrouter() {
  2. if (!this._router) {
  3. this._router = new Router({
  4. caseSensitive: this.enabled('case sensitive routing'),
  5. strict: this.enabled('strict routing')
  6. });
  7. this._router.use(query(this.get('query parser fn')));
  8. this._router.use(middleware.init(this));
  9. }
  10. };

这种如果没有再定义的策略,设计模式上叫单例模式,初次执行肯定没有,所以这里先会初始化一个rouer绑定到app._router上,caseSensitive 表示路由对大小写敏感,strict开启路由的严格模式,好,接着走到Router构造函数:

  1. var proto = module.exports = function(options) {
  2. var opts = options || {};
  3. function router(req, res, next) {
  4. router.handle(req, res, next);
  5. }
  6. // mixin Router class functions
  7. setPrototypeOf(router, proto)
  8. router.params = {};
  9. router._params = [];
  10. router.caseSensitive = opts.caseSensitive;
  11. router.mergeParams = opts.mergeParams;
  12. router.strict = opts.strict;
  13. router.stack = [];
  14. return router;
  15. };

router本身是一个函数,调用自身的而handle方法,传递http,setPrototypeOf 将router的__proto__属性指向proto,借此实现js式的继承,至于proto,也定义于此处,就是router的一系列方法,诸如param,handle,use等等。当然router也有自己的get,post等等方法,这些和app上定义的时候大同小异的,最后声明了几个保存变量的属性,将router返回了出来。初始化router完毕之后,router使用了两个中间件,第一个是parse query的,第二个中间件是用于初始化req和res的,他做了一件很重要的事就是将express框架的request和response 绑定到了当前的req和res上,部分代码如下:

  1. setPrototypeOf(req, app.request)
  2. setPrototypeOf(res, app.response)

这些方法里边包含了很多东西,诸如req的属性,res的send,set等等。
lazyRouter执行完毕,然后执行router上的route方法:

  1. function route(path) {
  2. var route = new Route(path);
  3. var layer = new Layer(path, {
  4. sensitive: this.caseSensitive,
  5. strict: this.strict,
  6. end: true
  7. }, route.dispatch.bind(route));
  8. layer.route = route;
  9. this.stack.push(layer);
  10. return route;
  11. };

Route是描述路由的一个数据结构,他的构造函数如下:

  1. function Route(path) {
  2. this.path = path;
  3. this.stack = [];
  4. debug('new %o', path)
  5. // route handlers for various http methods
  6. this.methods = {};
  7. }

path属性包含了当前路由的path,stack是定义路由是用于保存定义路由时生成的layer的数组,至于methods,举一个例子就是当 route.get()发生时,那么this.methods.get的值就是true。
接着会生成layer,layer的构造函数如下:

  1. function Layer(path, options, fn) {
  2. if (!(this instanceof Layer)) {
  3. return new Layer(path, options, fn);
  4. }
  5. debug('new %o', path)
  6. var opts = options || {};
  7. this.handle = fn;
  8. this.name = fn.name || '<anonymous>';
  9. this.params = undefined;
  10. this.path = undefined;
  11. this.regexp = pathRegexp(path, this.keys = [], opts);
  12. // set fast path flags
  13. this.regexp.fast_star = path === '*'
  14. this.regexp.fast_slash = path === '/' && opts.end === false
  15. }

这里边比较重要的一点是会将当前path转化成能匹配他的正则并把这个正则保存到this.regexp上,并且会把param参数在此处提取出来保存到this.keys上,还有一个handle属性保存在这个layer上执行的回调函数,以以上形式生成的layer上,其handle函数为route.dispatch.bind(route),这个函数是route实例上的方法,用于执行他的栈上保存的layer。然后layer生成完毕,此时的layer实例会将一个route属性指向当前的route,此刻,将这个layer保存到router的stacks里。

这里多提一句就是,router.use这种形式生成路由时layer上的route是undefined的,并且layer的handle就是传进去的回调函数。在后边router遍历自己stack上存储的layer时,正是基于此 判断他是中间件还是一个路由业务函数。

router.route执行完毕,接下来开始执行route[method].apply(route, slice.call(arguments, 1));, route[method]的定义方法,和app,router大同小异:

  1. methods.forEach(function(method){
  2. Route.prototype[method] = function(){
  3. var handles = flatten(slice.call(arguments));
  4. for (var i = 0; i < handles.length; i++) {
  5. var handle = handles[i];
  6. if (typeof handle !== 'function') {
  7. var type = toString.call(handle);
  8. var msg = 'Route.' + method + '() requires a callback function but got a ' + type
  9. throw new Error(msg);
  10. }
  11. debug('%s %o', method, this.path)
  12. var layer = Layer('/', {}, handle);
  13. layer.method = method;
  14. this.methods[method] = true;
  15. this.stack.push(layer);
  16. }
  17. return this;
  18. };
  19. });

route.get,post,…等方法也会生成layer,其handle就是定义的回调函数,然后这个layer会保存到route的stack里,在route的dispath方法里调用。
这样的话,route[method].apply(route, slice.call(arguments, 1));也就执行完毕了。至此,路由定义完毕。现在在这里梳理一下,app,router,route的关系:
此刻,app的内部属性_router上引用的router实例,他的stack上此刻应该有如下几个layer:

  • 用于parse query的中间件;
  • 用于初始化req,res的init中间件;

以上两个是初始化的时候就use的中间件,接下来是:

  • 调用app,get 的时候为route创造的layer。

app.get执行时,生成的route实例会在其stack上保存一个layer,该layer的handle就是我们定义的回调函数。
ok。下来app开始listen:

  1. app.listen = function listen() {
  2. var server = http.createServer(this);
  3. return server.listen.apply(server, arguments);
  4. };

server还是使用http模块的createServer创造的, 只不过this指向的是app也就是开头我们提到的整个程序的入口,是个函数,接着用了函数的apply方法,将app.listen调用时传过去的参数使用arguments巧妙的传过去,并将server设置为listen的上下文。listen之后,app就开始正式运行了,监听了9999端口。

——————

当在浏览器上输入http://localhost:9999/ 时,首先,app会被执行,而app里只有一句话就是app.handle(req, res, next); 参数分别是request,response和next(在此时为undefined),所以我们继续往下看app.handle:

  1. app.handle = function handle(req, res, callback) {
  2. var router = this._router;
  3. // final handler
  4. var done = callback || finalhandler(req, res, {
  5. env: this.get('env'),
  6. onerror: logerror.bind(this)
  7. });
  8. // no routes
  9. if (!router) {
  10. debug('no routes defined on app');
  11. done();
  12. return;
  13. }
  14. router.handle(req, res, done);
  15. };

开始执行 router.handle并将done默认值作为next参数传递过去: 此处代码稍长,所以还是将解释放到源码里边。

  1. function handle(req, res, out) {
  2. var self = this;
  3. debug('dispatching %s %s', req.method, req.url);
  4. var idx = 0;
  5. var protohost = getProtohost(req.url) || ''
  6. var removed = '';
  7. var slashAdded = false;
  8. var paramcalled = {};
  9. // store options for OPTIONS request
  10. // only used if OPTIONS request
  11. var options = [];
  12. // middleware and routes
  13. var stack = self.stack;
  14. // manage inter-router variables
  15. var parentParams = req.params;
  16. var parentUrl = req.baseUrl || '';
  17. /*重置传进来的next方法,restore的作用是保存初始的baseUrl,next,params的值,该方法最后返回
  18. 一个闭包函数,该闭包函数内req上的以上三个属性会被重置为初始值,然后调用out方法
  19. */
  20. var done = restore(out, req, 'baseUrl', 'next', 'params');
  21. // setup next layer
  22. req.next = next;
  23. // for options requests, respond with a default if nothing else responds
  24. if (req.method === 'OPTIONS') {
  25. done = wrap(done, function(old, err) {
  26. if (err || options.length === 0) return old(err);
  27. sendOptionsResponse(res, options, old);
  28. });
  29. }
  30. // setup basic req values
  31. req.baseUrl = parentUrl;
  32. req.originalUrl = req.originalUrl || req.url;
  33. /*开始执行next方法,遍历router.stack里的layer*/
  34. next();
  35. function next(err) {
  36. var layerError = err === 'route'
  37. ? null
  38. : err;
  39. // remove added slash
  40. if (slashAdded) {
  41. req.url = req.url.substr(1);
  42. slashAdded = false;
  43. }
  44. // restore altered req.url
  45. if (removed.length !== 0) {
  46. req.baseUrl = parentUrl;
  47. req.url = protohost + removed + req.url.substr(protohost.length);
  48. removed = '';
  49. }
  50. // signal to exit router
  51. if (layerError === 'router') {
  52. setImmediate(done, null)
  53. return
  54. }
  55. // no more matching layers
  56. if (idx >= stack.length) {
  57. setImmediate(done, layerError);
  58. // 遍历完毕,则在check阶段执行done方法
  59. return;
  60. }
  61. // get pathname of request
  62. var path = getPathname(req);
  63. if (path == null) {
  64. return done(layerError);
  65. }
  66. // find next matching layer
  67. var layer;
  68. var match;
  69. var route;
  70. /* 循环的目的,找匹配的layer,如果找不到匹配的layer就一直将stack里的layer遍历完毕 */
  71. while (match !== true && idx < stack.length) {
  72. layer = stack[idx++];
  73. /*是否匹配*/
  74. match = matchLayer(layer, path);
  75. route = layer.route;
  76. if (typeof match !== 'boolean') {
  77. // hold on to layerError
  78. layerError = layerError || match;
  79. }
  80. /*不匹配就开始下一轮循环*/
  81. if (match !== true) {
  82. continue;
  83. }
  84. /* 不是以app[method],router[method] 定义的路由就到此为止,跳出while,开始执行 */
  85. if (!route) {
  86. // process non-route handlers normally
  87. continue;
  88. }
  89. if (layerError) {
  90. // routes do not match with a pending error
  91. match = false;
  92. continue;
  93. }
  94. /*拿到http方法动词并判断是否为给出的动词*/
  95. var method = req.method;
  96. var has_method = route._handles_method(method);
  97. /* 如果不是已给出的方法动词,且为options,则在options数组里添加当前route.methods 的keys*/
  98. // build up automatic options response
  99. if (!has_method && method === 'OPTIONS') {
  100. appendMethods(options, route._options());
  101. }
  102. // don't even bother matching route
  103. /*如果不是已给出的方法动词且不是head 则将match重置为false开始下一个循环*/
  104. if (!has_method && method !== 'HEAD') {
  105. match = false;
  106. continue;
  107. }
  108. }
  109. // no match
  110. /* while循环执行完毕之后,有两种结果,第一种匹配到了layer,则开始执行layer,第二种,没有匹配到则执行done方法,重置req对象 */
  111. if (match !== true) {
  112. /*没有匹配到,*/
  113. return done(layerError);
  114. }
  115. // store route for dispatch on change
  116. /*当前为路由形式的layer,也就是layer.route不为undefined的layer,req.route 指向route*/
  117. if (route) {
  118. req.route = route;
  119. }
  120. // Capture one-time layer values
  121. /*获取当前req的params*/
  122. req.params = self.mergeParams
  123. ? mergeParams(layer.params, parentParams)
  124. : layer.params;
  125. var layerPath = layer.path;
  126. // this should be done for the layer
  127. /*process_params方法,有layer匹配当前param的时候,至多执行一次app.params()定义的方法,这些方法执行完毕之后,才开始执行当前的layer,也就是调用layer.handle_request*/
  128. self.process_params(layer, paramcalled, req, res, function (err) {
  129. if (err) {
  130. return next(layerError || err);
  131. }
  132. if (route) {
  133. /*路由形式的layer这么执行*/
  134. return layer.handle_request(req, res, next);
  135. }
  136. /*非路由形式的layer执行前,需要调用trim_prefix方法,该方法会先验证当前layer的path是否合法,然后重新设置req.urlreq.baseUrl等一系列工作,然后调用layout的方法*/
  137. trim_prefix(layer, layerError, layerPath, path);
  138. });
  139. }
  140. function trim_prefix(layer, layerError, layerPath, path) {
  141. if (layerPath.length !== 0) {
  142. // Validate path breaks on a path separator
  143. var c = path[layerPath.length]
  144. if (c && c !== '/' && c !== '.') return next(layerError)
  145. // Trim off the part of the url that matches the route
  146. // middleware (.use stuff) needs to have the path stripped
  147. debug('trim prefix (%s) from url %s', layerPath, req.url);
  148. removed = layerPath;
  149. req.url = protohost + req.url.substr(protohost.length + removed.length);
  150. // Ensure leading slash
  151. if (!protohost && req.url[0] !== '/') {
  152. req.url = '/' + req.url;
  153. slashAdded = true;
  154. }
  155. // Setup base URL (no trailing slash)
  156. req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
  157. ? removed.substring(0, removed.length - 1)
  158. : removed);
  159. }
  160. debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
  161. if (layerError) {
  162. layer.handle_error(layerError, req, res, next);
  163. } else {
  164. layer.handle_request(req, res, next);
  165. }
  166. }
  167. };

下来开始说layout的执行方法,首先对于正常的layer执行layer.handle_request(req, res, next)方法,其中next就是next函数,由于循环变量idx置于next函数外边,因此开始执行此函数就意味着开始寻找下一个匹配的layer。

  1. Layer.prototype.handle_request = function handle(req, res, next) {
  2. var fn = this.handle;
  3. if (fn.length > 3) {
  4. // not a standard request handler
  5. return next();
  6. }
  7. try {
  8. fn(req, res, next);
  9. } catch (err) {
  10. next(err);
  11. }
  12. };

由上面可以看到如果当前layer的handle的形参大于3就会出错,直接执行下一个layer,如果正确的话,则调用我们传递给他的回调函数。 handle在前面详细说过这里就不提了。
当执行完pase query,以及init 两个中间件之后,开始执行第三个我们定义的route,只不过这里的执行过程是 layer.handle -> router.dispatch -> 遍历route.stack里的layer执行,遍历的流程跟router上的差不多,然后最后就是执行:

  1. function indexHandler(req,res,next){
  2. res.set('Content-Type',"text/html;charset=utf-8");
  3. res.send(`<h1 style="color:red">hello world</h1>`)
  4. }

res的set使用来设置响应头的,各种响应头,至于send,除了为我们写了一些头还判断了http缓存逻辑,最后调用res.end()方法,将我们的<h1 style="color:red">hello world</h1> 返回给了客户端,剩下的工作是一些异步工作,诸如tcp挥手,node自己内部的一些方法,俺也没多做了解,就不展开讲了。至此hello world完成。

posted @ 2021-02-28 19:11  子龙_子龙  阅读(32)  评论(0编辑  收藏  举报