三步法解析Express源码

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 三步法解析Express源码

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


  1. 领悟思想:只需体会作者设计框架的初衷和目的
  2. 把握设计:只需体会代码的接口和抽象类以及宏观的设计
  3. 体会细节:是基于顶层的抽象接口设计,逐渐展开代码的画卷

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


一、领悟思想


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

  1. Express是基于Node.js平台,并且具备快速、极简的特点,说明其初衷就是为了通过扩展Node的功能来提高开发效率。
  2. 开放的特点说明该框架不会对开发者过多的限制,可以自由的发挥想象进行功能的扩展。
  3. Express是Web开发框架,说明作者的定位就是为了更加方便的帮助我们处理HTTP的请求和响应。


二、把握设计


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


2.1 源码目录


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


├─application.js---创建Express应用后可直接调用的api均在此处(核心)

├─express.js---入口文件,创建一个Express应用

├─request.js---丰富了http中request实例上的功能

├─response.js---丰富了http中response实例上的功能

├─utils.js---工具函数

├─view.js---与模板渲染相关的内容

├─router---与路由相关的内容(核心)

| ├─index.js

| ├─layer.js

| └route.js

├─middleware---与中间件相关的内容

| ├─init.js---会将新增加在request和response新增加的功能挂载到原始请求的request和response的原型上

| └query.js---将请求url中的query部分添加到request的query属性上


2.2 抽象接口


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

640.png


2.3 设计原理


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

640.jpg

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

const express = require('./express');
const res = require('./response');
const app = express();
app.get('/test1', (req, res, next) => {
    console.log('one');
    next();
}, (req, res) => {
    console.log('two');
    res.end('two');
})
app.get('/test2', (req, res, next) => {
    console.log('three');
    next();
}, (req, res) => {
    console.log('four');
    res.end('four');
})
app.listen(3000);
  1. Application
    表示一个Express应用,通过express()即可进行创建。
  2. Router
    路由系统,用于调度整个系统的运行,在上述代码中该路由系统包含app.get('/test1',……)和app.get('/test2',……)两大部分
  3. Layer
    代表一层,对于上述代码中app.get('/test1',……)和app.get('/test2',……)都可以成为一个Layer
  4. Route
    一个Layer中会有多个处理函数的情况,这多个处理函数构成了Route,而Route中的每一个函数又成为Route中的Layer。对于上述代码中,app.get('/test1',……)中的两个函数构成一个Route,每个函数又是Route中的Layer。


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

  1. 启动服务时会依次执行程序,将该路由系统中的路径、请求方法、处理函数进行存储(这些信息根据一定结构存储在Router、Layer和Route中)
  2. 对相应的地址进行监听,等待请求到达。
  3. 请求到达,首先根据请求的path去从上到下进行匹配,路径匹配正确则进入该Layer,否则跳出该Layer。
  4. 若匹配到该Layer,则进行请求方式的匹配,若匹配方式匹配正确,则执行该对应Route中的函数。


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


三、体会细节



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

640.png


3.1 初始化阶段


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


640.png


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


app.get = function(path){
    // ……
    this.lazyrouter();
    var route = this._router.route(path);
    route.get.apply(route, slice.call(arguments, 1));
    return this;
};
  1. 在app.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));
  }
};
  1. 注意:该过程中其实是利用了单例模式,保证整个过程中获取router实例的唯一性。


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


router.prototype.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;// 把route放到layer上
  this.stack.push(layer); // 把layer放到数组中
  return route;
};
  1. 将该app.get()中的函数存储到route的stack中。(注意源码中也是通过遍历method的方式将get挂载到route的prototype上)


Route.prototype.get = function(){
    var handles = flatten(slice.call(arguments));
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];
      // ……
      // 给route添加layer,这个层中需要存放方法名和handler
      var layer = Layer('/', {}, handle);
      layer.method = method;
      this.methods[method] = true;
      this.stack.push(layer);
    }
    return this;
  };

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


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

  1. 将路由处理方式(app.get()、app.post()……)、app.use()等划分为路由系统中的一个Layer。
  2. 对于每一个层中的处理函数全部存储至Route对象中,一个Route对象与一个Layer相互映射。


3.2 请求处理阶段


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

640.png


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


app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
  1. 当连接建立会调用app实例,app实例中会立即执行app.handle()函数,app.handle()函数会立即调用路由系统的处理函数router.handle()


app.handle = function handle(req, res, callback) {
  var router = this._router;
  // 如果路由系统中处理不了这个请求,就调用done方法
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });
  //……
  router.handle(req, res, done);
};
  1. router.handle()主要是根据路径获取是否有匹配的layer,当匹配到之后则调用layer.prototype.handle_request()去执行route中内容的处理


router.prototype.handle = function handle(req, res, out) {
  // 这个地方参数out就是done,当所有都匹配不到,就从路由系统中出来,名字很形象
  var self = this;
  // ……
  var stack = self.stack;
  // ……
  next();
  function next(err) {
    // ……
    // get pathname of request
    var path = getPathname(req);
    // find next matching layer
    var layer;
    var match;
    var route;
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path);
      route = layer.route;
      // ……
    }
    // no match
    if (match !== true) {
      return done(layerError);
    }
    // ……
    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }
      if (route) {
        return layer.handle_request(req, res, next);
      }
      trim_prefix(layer, layerError, layerPath, path);
    });
  }
  function trim_prefix(layer, layerError, layerPath, path) {
    // ……
    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};
  1. layer.handle_request()会调用route.dispatch()触发route中内容的执行


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);
  }
};
  1. route中的通过判断请求的方法和route中layer的方法是否匹配,匹配的话则执行相应函数,若所有route中的layer都不匹配,则调到外层的layer中继续执行。


Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if (stack.length === 0) {
    return done();
  }
  var method = req.method.toLowerCase();
  // ……
  next();
  // 此next方法是用户调用的next,如果调用next会执行内层的next方法,如果没有匹配到会调用外层的next方法
  function next(err) {
    // ……
    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }
    if (layer.method && layer.method !== method) {
      return next(err);
    }
    // 如果当前route中的layer的方法匹配到了,执行此layer上的handler
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};


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

  1. 首先判断layer中的path和请求的path是否一致,一致则会进入route进行处理,否则调到下一层layer
  2. 在route中会判断route中的layer与请求方法是否一致,一致的话则函数执行,否则不执行,所有route中的layer执行完后跳到下层的layer进行执行
相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
108 2
|
26天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
26天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
26天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2天前
|
自然语言处理 数据处理 索引
mindspeed-llm源码解析(一)preprocess_data
mindspeed-llm是昇腾模型套件代码仓,原来叫"modelLink"。这篇文章带大家阅读一下数据处理脚本preprocess_data.py(基于1.0.0分支),数据处理是模型训练的第一步,经常会用到。
9 0
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
65 12
|
1月前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
27天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
2月前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
2月前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
70 3

热门文章

最新文章

推荐镜像

更多