Skip to content

Node.js系列专题之Express分析 #7

@GeniusFunny

Description

@GeniusFunny

Node.js系列专题之Express源码解读

介绍

Node.js的发展历史

2009年,Node.js诞生

2010年,Express和Socket.io诞生

2011年,Node.js正式商用(LinkedIn、Uber)、Npm诞生

2012年,Node.js趋于成熟,商用框架Hapi诞生

2013年,Node.js在Web框架领域百花齐放,Koa诞生;蚂蚁金服内部推出Chair框架(Egg.js前身)

2016年,Egg.js开源

定义

官方对Express的描述:Fast, unopinionated, minimalist web framework for Node.js

我们可以从中看出,Express主要有2个特点:

  • 快速极简:通过扩展 Node 的功能来提高开发效率
  • 高度包容:框架不会对开发者过多的限制,可以自由发挥想象的空间

Demo

const express = require('express')
const app = express() // 实际执行 createcreateApplication
const port = 3000

app.use(function(req, res, next) {
  // ...
  next()
})
app.use('/test', function(req, res, next) {
  // ...
  next()
})


app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Demo非常简单,主要步骤如下:

1.引入express模块

2.调用express(),创建一个应用实例

3.通过app.use绑定两个中间件

4.通过app.get为根目录注册一个Get路由

5.通过app.listen将服务运行在3000端口,并监听请求

源码机构

Express源码合计约3500行。

- lib/
    - middleware/
        - init.js
        - query.js
    - router/
        - index.js
        - layer.js
        - route.js
    - application.js
    - express.js
    - request.js
    - response.js
    - utils.js
    - view.js
- index.js

  • middleware目录下包含两个默认的中间件
  • router目录下主要是路由功能的源码【核心】
  • request/response主要是简化和增强了http模块原始的req/res对象
  • application.js跟应用实例相关,掌握整个程序的流程控制【核心】
  • express.js里面暴露了createApplication这个工厂函数,用于创建应用实例
  • utils/view则分别是express框架所用到的工具函数和模板引擎代码
  • index.js仅仅是将express.js的导出再此导出

核心实现

下面主要从程序运行角度,将express的源码分为两个阶段:初始化流程请求处理

初始化流程

创建应用实例
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false); // 赋予eventEmitter能力
  mixin(app, proto, false); // 挂载基本方法,例如get/post/listen/init/use/handle等

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })
	
  app.init(); // 初始化
  return app;
}

Demo中调用的express()即执行的createApplication函数;

  1. 在createApplication函数执行过程中,app这个函数被挂载了许多方法和属性。

  2. 然后调用app.init()方法进行初始化。

  3. 最后返回app函数。(为什么返回的是函数而不是Object?)

挂载中间件(一)
app.use = function (fn) {
  // 通过offset来处理函数重载,忽略细节
  var fns = flatten(slice.call(arguments, offset));
  this.lazyrouter() // 初始化router,讲路由的时候再说
  fns.forEach(fn => {
    // 实际上app.use调用的router的中间件绑定
    this.router.use(path, function mounted_app(req, res, next) {
    	fn.handle(req, res, function(err) {
      setPrototypeOf(req, orig.request)
      setPrototypeOf(res, orig.response)
      next(err);
    })
  })
  fn.emit('mount', this);
  })
}

绑定中间件:

  1. 通过对第一个实参的类型进行判断进而达到函数重载的效果
    • 若为string类型,则第一个参数为 path(默认’/’);offset设置为1
    • 若为function类型,则参数都为中间件;offset设置为0
  2. 遍历中间件数组,依次调用router.use方法进行绑定(app.use实际执行router.use)
创建Router
// 单例模式
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    // 初始化一个router实例,全局唯一
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });
		// router挂载基本中间件
    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

通过单例模式初始化了一个Router实例,然后挂载基本中间件。

Router对象内部结构:

function Router(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

Router构造函数:

  1. 与createApplication类似,返回的也是函数而非object

  2. 内部维护了stack数组

挂载中间件(二)

app.use内部实际执行的router.use

// router.use(fn1, fn2); // 所有路径和方法(除options)都会执行
// router.use('/test', fn3); // 针对/test才会执行

// 伪代码
router.use = function(callbacks) {
  // 
  for(let i = 0; i < callbacks.length; i++) {
    const layer = new Layer(path, {
      // 配置
    }, callbacks[i]);
    layer.route = undefined;
    this.stack.push(layer); // 将所有中间件维护在一起
  }
}

1.与app.use类似,通过对参数的判断进行函数重载

2.对每个中间件包装成Layer对象,Layer对象的route属性设置为undefined;

3.将layer对象维护在router的stack数组中

这里的layer对象内部机构如下:

function layer(path, options, fn) {
  // 省略部分代码
   this.handle = fn;
   this.path = undefined;
}

Layer构造函数: 接受3个参数,第一个是路径,默认为undefined;最后一个是处理函数

注册路由
  1. methods为HTTP方法数组,源码灵活的为app绑定注册路由的方法
  2. 注册路由实际还是调用的router内部的方法即router.route
  3. 执行route[method]方法,为route绑定处理函数
app[method] = function(path){
  if (method === 'get' && arguments.length === 1) {
    // app.get(setting)
      return this.set(path);
   }
    this.lazyrouter();
    var route = this._router.route(path); // 初始化route
    route[method].apply(route, slice.call(arguments, 1)); // 绑定处理函数
    return this;
}

初始化route:

1.创建指定path的route对象

2.创建指定path的layer对象,并且该layer绑定的函数为route.dispatch

3.并将layer推入router的stack数组

app.use绑定中间件时创建layer时,将layer.route设置为undefined,注册路由时却将layer.route设置为route。

由此可知,Router的layer有2种类型,一种非路由中间件(未指定route),一种则为路由中间件(指定route)。

router.route = function route(path) {
  let route = new Route(path);
  let layer = new Layer(path, {
    // 配置
  }, route.dispatch.bind(route)) // layer上绑定route的dispatch方法【后续会说作用】
  layer.route = route; // layer只与此route相关
  this.stack.push(layer); // 将一个route下的所有路由函数放一起
  return route;
}

Route对象内部结构:

  • 必须指定path
  • 拥有stack数组
  • 还有一个methods对象
function Route(path) {
  // 忽略部分代码
  this.path = path;
  this.stack = []; // route也有stack,用来存储处理函数,即对应 http路由的 处理函数
  // route handlers for various http methods
  this.methods = {};
}
注册路由处理函数

Route为指定方法绑定处理函数:

  1. 同时利用了methods数组,遍历绑定
  2. 用layer包装处理函数,使用stack统一管理
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires a callback function but got a ' + type
        throw new Error(msg);
      }

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});
总结

经过初始化阶段后,各个模块关系图如下:

初始化模块关系图.png

  • Router下面有多个Layer(可以理解为中间件),中间件分2大类:
    • 通过app.use或者router.use绑定的非路由中间件;中间件的功能由开发者自定义;
    • (以get为例)通过app. get或者router.get绑定的路由中间件;中间件是由框架指定Route.dispatch方法;
  • Route下面也有多个Layer(也可以理解为中间件),即注册路由时的回调函数;例如:app.get(‘/test’, fn); fn就是回调函数

请求处理

端口监听

Express将创建server和监听端口合并成一个app.listen函数

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

并且,创建server时 将app函数本身作为requestListener,那么每次端口监听到请求时就会执行app函数;

http.createServer

这样就进入express的第二个阶段,请求处理阶段

app.handle

请求处理

app.handle内部实际调用的router.handle

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  // no routes
  if (!router) {
 debug('no routes defined on app');
    done();
 return;
  }

  router.handle(req, res, done);
};

router.handle主要做两件事情:1. 根据传进来的参数req,做一些处理,如获取ur;2.遍历layer,这个是通过next函数来实现。

router.handle

next函数:按索引遍历Router.stack,找到匹配路径的中间件,然后执行layer.handle_request(req, res, next)

router_next

handle_request函数其实就是执行创建layer对象时传递的第三个参数(handle)。

  • 路由中间件的layer绑定的handle为route.dispatch

  • 非路由中间件的layer绑定的handle为自定义函数

layer.prototype.handle_request = function(req, res, next) {
  var fn = this.handle;
  if (fn.length > 3) return next(); // 如果参数不对,跳过本处理函数
  try {
    fn(req, res, next); // 执行处理函数
  } catch(err) {
    next(err);
  }
}

再来看route.dispatch: dispatch的功能就是,遍历route的stack数组,若layer的method和本次req的method匹配就执行。

// dispatch req, res into this route
Route.prototype.dispatch = function(req, res, next) {
  if (stack.length === 0) return;
  // ...不必要细节
  function next(err) {
    var layer = stack[idx++];
    // method就是http方法
    if (layer.method && layer.method !== method) { // 方法不匹配,跳过本处理函数
      return next(err);
    }
    layer.handle_request(req, res, next); // 执行处理函数
  }
  next()
}
总结

请求处理流程如图:

processing

  1. app监听端口,有request时,交给router处理。
  2. router处理步骤:
    1. 遍历其layer数组,找到匹配的路径的layer,根据不同类型layer做出不同的操作
      1. 路由中间件:执行匹配的layer然后将控制权移交Route,遍历Route的layer数组,执行完与request.method方法匹配的所有layer;最后控制权交还给Router
      2. 非路由中间件:执行匹配的layer

优/缺点

优点

  • 易上手: express对web开发相关的模块进行了适度的封装,屏蔽了大量复杂繁琐的技术细节,让开发者只需要专注于业务逻辑的开发,极大的降低了入门和学习的成本。
  • 高性能: express仅在web应用相关的Node.js模块上进行了适度的封装和扩展,较大程度避免了过度封装导致的性能损耗。
  • 扩展性强:**基于中间件的开发模式,使得express应用的扩展、模块拆分非常简单,既灵活,扩展性又强

缺点

  • 基于callback组合业务逻辑,业务逻辑复杂时嵌套过多,回调地狱蛋疼;
  • 没有统一的错误处理机制,error-first贯穿整个应用
  • Express团队维护频率低(推出了Koa)

总结

作为Node.js平台第一个Web应用框架,也是多数JavaScripter接触的第一个服务端框架,Express已经完成了它的历史使命。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Node.jssomething about Node.js

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions