Skip to content

Express源码解读03 - application

"version": "4.18.1",

1. 初始化应用app.init

1. createApplicationapp.init()的来源

express.js中,创建应用createApplication方法最后有一个方法我们还没有分析到,就是app.init(),调用express()后,内部会自动的执行app.init()方法。

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

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false); //  false 不合并静态属性,

  // 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;
}

这节里面主要分析init(),所以在createApplication里面,主要看的代理就两行

js
mixin(app, proto, false)
app.init();

mixin(app, proto, false)上节已经说过,proto其实就是appmixin的作用就是将两个地方的app进行对象的合并

mixin合并了app后,在app里面就有了application里面的属性及方法,因此,下面的app.init()方法就是./application.js里面暴露的。(createApplication里面已经将application分离出去了)

2. app.init()干了啥?

可以看到,内部在express()调用的时候,就会执行app.init()init方法的定义在application里面,具体如下

js
app.init = function init() {
  this.cache = {};    // 缓存
  this.engines = {};  // 模板引擎
  this.settings = {}; // 设置
  this.defaultConfiguration(); // 设置一些默认配置,settings里面的东西
};

这里的this指向的是app 初始化就是声明了三个对象,分别是缓存、模板引擎、配置,均是空对象 然后defaultConfiguration方法的作用是得到一些默认配置

defaultConfiguration其实就是利用app.set一堆设置setting默认值的操作,跑完defaultConfiguration方法后,为this.settings对象设置了如下初始值:

js
settings:{
  "x-powered-by": true,
  etag: "weak",
  "etag fn": function generateETag (body, encoding) {
    var buf = !Buffer.isBuffer(body)
      ? Buffer.from(body, encoding)
      : body
    
    return etag(buf, options)
  },
  env: "development",
  "query parser": "extended",
  "query parser fn": function parseExtendedQueryString(str) {
    return qs.parse(str, {
      allowPrototypes: true
    });
  },
  "subdomain offset": 2,
  "trust proxy": false,
  "trust proxy fn": function trustNone () {
    return false
  },
}

2. app.set()原理

在express中,提供了 app.set(name,value) 这样的API,可以存储任何想要的值,这在源码中有是这样实现的。

js
app.set = function set(setting, val) {
  if (arguments.length === 1) { // 若只有一个参数,是取值操作
    // app.get(setting)
    var settings = this.settings;

    // 根据原型链向上递归查找
    while (settings && settings !== Object.prototype) {
      if (hasOwnProperty.call(settings, setting)) {
        return settings[setting];
      }

      settings = Object.getPrototypeOf(settings); // 获取settings的原型给settings
    }

    return undefined;
  }

  debug('set "%s" to %o', setting, val); // 输出

  // set value
  this.settings[setting] = val;

  // trigger matched settings
  switch (setting) {
    case "etag":
      this.set("etag fn", compileETag(val));
      break;
    case "query parser":
      this.set("query parser fn", compileQueryParser(val));
      break;
    case "trust proxy":
      this.set("trust proxy fn", compileTrust(val));

      // trust proxy inherit back-compat
      Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
        configurable: true,
        value: false,
      });

      break;
  }

  return this;
};

可以看出,源码里面app.set()方法,不仅做了存值操作,也做了取值操作,就是根据判断有几个参数来实现的。取值会根据原型链层层向上查找,最终都找不到,才会返回undefined

并且,若是设置etagquery parsertrust proxy的值,还会当独设置一个对应的处理函数在settings里面

boolean类型的值时候进行设置值和取值的时候,为了简化操作,源码里面还提供四个方法,app.enabledapp.enableapp.disabledapp.disable,具体如下:

js
// 取值操作
app.enabled = function enabled(setting) { // 取值,返回布尔值
  return Boolean(this.set(setting));
};

app.disabled = function disabled(setting) { // 取值, 返回相反的布尔值
  return !this.set(setting);
};

// 设置值
app.enable = function enable(setting) {  // 设置setting为true
  return this.set(setting, true);
};

app.disable = function disable(setting) { // 设置setting为false
  return this.set(setting, false);
};

ed的为取值,否则为设置值

取值还可以通过app.get()app.get来操作,但是他们都只能传一个参数

那么问题来了,app.get不就和路由的get请求冲突了吗?是的,确实会与GET请求冲突,所以,源码里面是做了这样的操作

在路由匹配里面做了如下判断,来区分是去取值还是真正的路由。

js
if (method === "get" && arguments.length === 1) {
    // app.get(setting)
    return this.set(path);
}

3. 路由匹配

3.1 GET、POST、PUT、DELETE...匹配

路由在express里面也是内置的功能,不用单独安装,我们平时开发时,路由会有很多方法,比如GETPOSTDELETEPUT等等,那么在express里面,是如来根据不同的路由来返回给客户端不同资源的呢?

路由系统在Express里面占了很大的比重,这里仅仅只是分析根据不同请求方法来匹配的方式,后面会有章节专门分析Express里面的路由。

application.js里面,也做了路由的匹配,源码如下:

js
methods.forEach(function (method) {
  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[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

其中,methods是一个第三方的包,里面定义了所有请求方式的动词

可以看出,Express里面是通过循环的方式,将请求方式一个个的挂载在app上面的,当请求进来的时候,会自己去走请求方式对应的方法,至于其他的很多有关路由匹配的解释,这里暂时不做过多解释,后面文章会详细说到。

lazyrouter()方法是加载路由的,见名知意就是懒加载路由,由于express是自带路由系统的,正常应该是创建应用,就会加载路由,但是为了提高性能考虑,并非一创建应用就会加载路由系统,而是在用到了路由的时候,才会去加载。因此称作懒加载。

3.2 路由all方法

Express里面有这样的一种路由,app.all()方法,无论什么请求方式,只要请求的路径能匹配到all里面的path,就会走对应的路由处理函数,原理就是比路由方法匹配少了一个方法的匹配,直接走的路径匹配。

js
app.all = function all(path) {
  this.lazyrouter();

  var route = this._router.route(path);
  var args = slice.call(arguments, 1);

  for (var i = 0; i < methods.length; i++) {
    route[methods[i]].apply(route, args);
  }

  return this;
};

4. listen方法

每个WEB应用程序要与外界通信,都需要有一个端口,这里的端口指的是虚拟端口(其它还有物理端口,可以理解为插口),listen方法是用来监听端口的

Expresshttp模块进行了封装,当调用app.listen()方法就会返回一个 http.Server 对象,所以可以直接使用app.listen(port)来监听端口并启动应用程序。

js
app.listen = function listen() {
  var server = http.createServer(this); // 创建http Serve对象,传入的是应用app
  return server.listen.apply(server, arguments); // 返回原生Nodejs的应用程序
};

可以看到,上面做的一系列有关app的操作,最终都是为创建http服务器对象做准备的

也就是说,只要一运行app.listen,就会启动一个http服务器,express()顶多就是创建一个应用程序,但是这个应用程序并没有启动,不能对外提供服务。