三步法解析Express源码

开发 后端
在抖音上有幸看到一个程序员讲述如何阅读源代码,主要分为三步:领悟思想、把握设计、体会细节。

 [[349860]]

在抖音上有幸看到一个程序员讲述如何阅读源代码,主要分为三步:领悟思想、把握设计、体会细节。

领悟思想:只需体会作者设计框架的初衷和目的

把握设计:只需体会代码的接口和抽象类以及宏观的设计

体会细节:是基于顶层的抽象接口设计,逐渐展开代码的画卷

基于上述三步法,迫不及待的拿Express开刀了。本次源码解析有什么不到位的地方各位读者可以在下面留言,我们一起交流。

一、领悟思想

在Express中文网上,介绍Express是基于Node.js平台,快速、开放、极简的Web开发框架。在这句话里面可以得到解读出以下几点含义:

Express是基于Node.js平台,并且具备快速、极简的特点,说明其初衷就是为了通过扩展Node的功能来提高开发效率。

开放的特点说明该框架不会对开发者过多的限制,可以自由的发挥想象进行功能的扩展。

Express是Web开发框架,说明作者的定位就是为了更加方便的帮助我们处理HTTP的请求和响应。

二、把握设计

理解了作者设计的思想,下面从源码目录、核心设计原理及抽象接口三个层面来对Express进行整体的把握。

2.1 源码目录

如下所示是Express的源码目录,相比较来说还是比较简单的。

  1. ├─application.js---创建Express应用后可直接调用的api均在此处(核心) 
  2. ├─express.js---入口文件,创建一个Express应用 
  3. ├─request.js---丰富了http中request实例上的功能 
  4. ├─response.js---丰富了http中response实例上的功能 
  5. ├─utils.js---工具函数 
  6. ├─view.js---与模板渲染相关的内容 
  7. ├─router---与路由相关的内容(核心) 
  8. | ├─index.js 
  9. | ├─layer.js 
  10. | └route.js 
  11. ├─middleware---与中间件相关的内容 
  12. | ├─init.js---会将新增加在request和response新增加的功能挂载到原始请求的request和response的原型上 
  13. | └query.js---将请求url中的query部分添加到request的query属性上 

2.2 抽象接口

对源码的目录结构有了一定了解,下面利用UML类图对该系统各个模块的依赖关系进一步了解,为后续源码分析打好基础。

 

2.3 设计原理

这一部分是整个Express框架的核心,下图是整个框架的运行流程,一看是不是很懵逼,为了搞清楚这一部分,需要明确四个概念:Application、Router、Layer、Route。


 

 

为了明确上述四个概念,先引入一段代码

  1. const express = require('./express'); 
  2. const res = require('./response'); 
  3. const app = express(); 
  4. app.get('/test1', (req, res, next) => { 
  5.     console.log('one'); 
  6.     next(); 
  7. }, (req, res) => { 
  8.     console.log('two'); 
  9.     res.end('two'); 
  10. }) 
  11. app.get('/test2', (req, res, next) => { 
  12.     console.log('three'); 
  13.     next(); 
  14. }, (req, res) => { 
  15.     console.log('four'); 
  16.     res.end('four'); 
  17. }) 
  18. app.listen(3000); 

Application

表示一个Express应用,通过express()即可进行创建。

Router

路由系统,用于调度整个系统的运行,在上述代码中该路由系统包含app.get('/test1',……)和app.get('/test2',……)两大部分

Layer

代表一层,对于上述代码中app.get('/test1',……)和app.get('/test2',……)都可以成为一个Layer

Route

一个Layer中会有多个处理函数的情况,这多个处理函数构成了Route,而Route中的每一个函数又成为Route中的Layer。对于上述代码中,app.get('/test1',……)中的两个函数构成一个Route,每个函数又是Route中的Layer。

了解完上述概念后,结合该幅图,就大概能对整个流程有了直观感受。首先启动服务,然后客户端发起了http://localhost:3000/test2的请求,该过程应该如何运行呢?

启动服务时会依次执行程序,将该路由系统中的路径、请求方法、处理函数进行存储(这些信息根据一定结构存储在Router、Layer和Route中)

对相应的地址进行监听,等待请求到达。

请求到达,首先根据请求的path去从上到下进行匹配,路径匹配正确则进入该Layer,否则跳出该Layer。

若匹配到该Layer,则进行请求方式的匹配,若匹配方式匹配正确,则执行该对应Route中的函数。

上述解释的比较简单,后续会在细节部分进一步阐述。

三、体会细节

通过上述对Express设计原理的分析,下面将从两个方面做进一步的源码解读,下面流程图是一个常见的Express项目的过程,首先会进行app实例初始化、然后调用一系列中间件,最后建立监听。对于整个工程的运行来说,主要分为两个阶段:初始化阶段、请求处理阶段,下面将以app.get()为例来阐述一下该核心细节。


 

 

3.1 初始化阶段

下面利用app.get()这个路由来了解一下工程的初始化阶段。

 

首先来看一下app.get()的内容(源代码中app.get()是通过遍历methods的方式产生)

  1. app.get = function(path){ 
  2.     // …… 
  3.     this.lazyrouter(); 
  4.  
  5.     var route = this._router.route(path); 
  6.     route.get.apply(route, slice.call(arguments, 1)); 
  7.     return this; 
  8. }; 

在app.lazyrouter()会完成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.  
  8.     // 此处会使用一些中间件 
  9.     this._router.use(query(this.get('query parser fn'))); 
  10.     this._router.use(middleware.init(this)); 
  11.   } 
  12. }; 

注意:该过程中其实是利用了单例模式,保证整个过程中获取router实例的唯一性。

调用router.route()方法完成layer的实例化、处理及保存,并返回实例化后的route。(注意源码中是proto.route)

  1. router.prototype.route = function route(path) { 
  2.   var route = new Route(path); 
  3.   var layer = new Layer(path, { 
  4.     sensitive: this.caseSensitive, 
  5.     strict: this.strict, 
  6.     endtrue 
  7.   }, route.dispatch.bind(route)); 
  8.  
  9.   layer.route = route;// 把route放到layer上 
  10.  
  11.   this.stack.push(layer); // 把layer放到数组中 
  12.   return route; 
  13. }; 

将该app.get()中的函数存储到route的stack中。(注意源码中也是通过遍历method的方式将get挂载到route的prototype上)

  1. Route.prototype.get = function(){ 
  2.     var handles = flatten(slice.call(arguments)); 
  3.  
  4.     for (var i = 0; i < handles.length; i++) { 
  5.       var handle = handles[i]; 
  6.       // …… 
  7.       // 给route添加layer,这个层中需要存放方法名和handler 
  8.       var layer = Layer('/', {}, handle); 
  9.       layer.method = method; 
  10.  
  11.       this.methods[method] = true
  12.       this.stack.push(layer); 
  13.     } 
  14.  
  15.     return this; 
  16.   }; 

注意:上述代码均删除了源码中一些异常判断逻辑,方便读者看清整体框架。

通过上述的分析,可以看出初始化阶段主要做了两件事情:

将路由处理方式(app.get()、app.post()……)、app.use()等划分为路由系统中的一个Layer。

对于每一个层中的处理函数全部存储至Route对象中,一个Route对象与一个Layer相互映射。

3.2 请求处理阶段

当服务启动后即进入监听状态,等待请求到达后进行处理。

 

app.listen()使服务进入监听状态(实质上是调用了http模块)

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

当连接建立会调用app实例,app实例中会立即执行app.handle()函数,app.handle()函数会立即调用路由系统的处理函数router.handle()

  1. app.handle = function handle(req, res, callback) { 
  2.   var router = this._router; 
  3.   // 如果路由系统中处理不了这个请求,就调用done方法 
  4.   var done = callback || finalhandler(req, res, { 
  5.     env: this.get('env'), 
  6.     onerror: logerror.bind(this) 
  7.   }); 
  8.   //…… 
  9.   router.handle(req, res, done); 
  10. }; 

router.handle()主要是根据路径获取是否有匹配的layer,当匹配到之后则调用layer.prototype.handle_request()去执行route中内容的处理

  1. router.prototype.handle = function handle(req, res, out) { 
  2.   // 这个地方参数out就是done,当所有都匹配不到,就从路由系统中出来,名字很形象 
  3.   var self = this; 
  4.   // …… 
  5.   var stack = self.stack; 
  6.    
  7.   // …… 
  8.  
  9.   next(); 
  10.  
  11.   function next(err) { 
  12.     // …… 
  13.     // get pathname of request 
  14.     var path = getPathname(req); 
  15.  
  16.     // find next matching layer 
  17.     var layer; 
  18.     var match; 
  19.     var route; 
  20.  
  21.     while (match !== true && idx < stack.length) { 
  22.       layer = stack[idx++]; 
  23.       match = matchLayer(layer, path); 
  24.       route = layer.route; 
  25.       // …… 
  26.     } 
  27.  
  28.     // no match 
  29.     if (match !== true) { 
  30.       return done(layerError); 
  31.     } 
  32.     // …… 
  33.  
  34.     // Capture one-time layer values 
  35.     req.params = self.mergeParams 
  36.       ? mergeParams(layer.params, parentParams) 
  37.       : layer.params; 
  38.     var layerPath = layer.path; 
  39.  
  40.     // this should be done for the layer 
  41.     self.process_params(layer, paramcalled, req, res, function (err) { 
  42.       if (err) { 
  43.         return next(layerError || err); 
  44.       } 
  45.  
  46.       if (route) { 
  47.         return layer.handle_request(req, res, next); 
  48.       } 
  49.  
  50.       trim_prefix(layer, layerError, layerPath, path); 
  51.     }); 
  52.   } 
  53.  
  54.   function trim_prefix(layer, layerError, layerPath, path) { 
  55.     // …… 
  56.  
  57.     if (layerError) { 
  58.       layer.handle_error(layerError, req, res, next); 
  59.     } else { 
  60.       layer.handle_request(req, res, next); 
  61.     } 
  62.   } 
  63. }; 

layer.handle_request()会调用route.dispatch()触发route中内容的执行

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

route中的通过判断请求的方法和route中layer的方法是否匹配,匹配的话则执行相应函数,若所有route中的layer都不匹配,则调到外层的layer中继续执行。

  1. Route.prototype.dispatch = function dispatch(req, res, done) { 
  2.   var idx = 0; 
  3.   var stack = this.stack; 
  4.   if (stack.length === 0) { 
  5.     return done(); 
  6.   } 
  7.  
  8.   var method = req.method.toLowerCase(); 
  9.   // …… 
  10.      
  11.   next(); 
  12.   // 此next方法是用户调用的next,如果调用next会执行内层的next方法,如果没有匹配到会调用外层的next方法 
  13.   function next(err) { 
  14.     // …… 
  15.  
  16.     var layer = stack[idx++]; 
  17.     if (!layer) { 
  18.       return done(err); 
  19.     } 
  20.  
  21.     if (layer.method && layer.method !== method) { 
  22.       return next(err); 
  23.     } 
  24.  
  25.     // 如果当前route中的layer的方法匹配到了,执行此layer上的handler 
  26.     if (err) { 
  27.       layer.handle_error(err, req, res, next); 
  28.     } else { 
  29.       layer.handle_request(req, res, next); 
  30.     } 
  31.   } 
  32. }; 

通过上述的分析,可以看出初始化阶段主要做了两件事情:

  1. 首先判断layer中的path和请求的path是否一致,一致则会进入route进行处理,否则调到下一层layer
  2. 在route中会判断route中的layer与请求方法是否一致,一致的话则函数执行,否则不执行,所有route中的layer执行完后跳到下层的layer进行执行。

本文转载自微信公众号「执鸢者」,可以通过以下二维码关注。转载本文请联系执鸢者公众号。

 

责任编辑:武晓燕 来源: 执鸢者
相关推荐

2021-09-04 23:26:26

源码ExpressNode

2021-09-04 23:27:58

Axios源码流程

2010-11-22 10:57:57

职场

2019-10-24 10:00:13

归类分组分解问题代码

2022-05-10 11:31:44

经营分析财务指标

2022-11-02 13:16:58

数据分析

2021-03-02 07:02:45

Linux操作系统

2012-10-31 09:31:06

SSD使用寿命固态存储

2023-09-26 12:32:21

数据分析领导数据

2012-12-24 09:49:24

2012-08-08 17:05:36

App运营

2011-06-28 11:03:55

邮件归档

2020-11-04 00:00:29

Kerberos协议身份

2009-02-04 09:45:05

Java SocketSocket APIJava编程

2011-07-13 09:54:22

VMware故障vSphere

2022-05-21 23:56:16

Python库搜索Python

2009-10-12 12:45:36

RHEL5 Squid

2023-05-19 15:51:36

数据治理工具

2010-06-08 10:37:15

云计算风险

2013-12-26 13:10:38

大数据NoSQL
点赞
收藏

51CTO技术栈公众号