解读express 4.x源码(1)


这两天仔细看了看express的源码,对其的整个实现有了较清晰的认识,所以想总结一下写出来,如果有什么不对的地方,望指出。

这是第一篇,首先介绍一个最简单的express应用运行过程,初步分析了其在源码中的具体实现,还没有涉及到一些比较重要的内容比如路由组件的实现方式,中间件的触发流程等。在后续的总结中,我会继续分析,并准备将一些值得分析的public api逐一解读,也会涉及一些private api

基于的版本

截止写这篇文章时目前最新的tags是4.4.2。我是直接看的master分支。express的commits提交非常频繁,但总体的实现思路应该不会有大的变化。其在4.x后做了较大的改动,相对于3.x最大的地方在于不再依赖connect,并移除了几乎所有的内置中间件,具体的变动请看官方wiki的 Migrating from 3.x to 4.xNew features in 4.x

从一个官方示例开始

var express = require('express');
var app = express();

app.get('/', function(req, res){
  res.send('Hello World');
});

app.listen(3000);

这是官方给出的一个简单程序,运行后访问localhost:3000显示Hello World。下面我们就来仔细看看这段程序。

首先第一行

var express = require('express');

这是典型的Node.js模块载入代码,关于Node.js的模块载入机制,不了解的同学建议看看朴灵的深入Node.js的模块机制,非常有帮助。

第一行载入了express框架,我们来看源代码中的index.js

module.exports = require('./lib/express');

好吧,还要继续require,我们看./lib/express.js

exports = module.exports = createApplication;

从这里我们可以看出,程序的第一行express最后实际是这个createApplication函数。第二行则是运行了这个函数,然后返回值赋给了app。该函数代码如下

var EventEmitter = require('events').EventEmitter;
var mixin = require('utils-merge');
var proto = require('./application');
var req = require('./request');
var res = require('./response');

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, proto);
  mixin(app, EventEmitter.prototype);

  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}

可以发现,这个就相当于express'main'函数,其中完成了所有创建express实例所需要的动作,并在执行完毕后返回一个函数。

代码的开始定义了一个函数,函数有形参reqresnext为回调函数。
函数体只有一条语句,执行app.handlehandle方法在application.js文件中定义,此处是通过mixin导入(见下文),handle的代码如下

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

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

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

    // generate error    
    var err = new Error('No routes or middlewares have been defined');
    err.status = 500;
    done(err);
    return;
  }

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

它的作用就是将每对[req,res]进行逐级分发,作用在每个定义好的路由及中间件上,直到最后完成,具体的过程我们会在后续进行分析。

然后来看看中间的两行

mixin(app, proto);
mixin(app, EventEmitter.prototype);

mixin是在头部的require处载入的utils-merge模块,它的代码如下

exports = module.exports = function(a, b){
  if (a && b) {
    for (var key in b) {
      a[key] = b[key];
    }
  }
  return a;
};

很明显,mixin(app, proto);的作用即是将proto中所有的property全部导入进appproto在头部的require处载入的是./lib/application.js文件,其中定义了大部分expresspublic api,如app.set,app.get,app.use…详见官方的API文档
mixin(app, EventEmitter.prototype);则将Node.jsEventEmitter中的原型方法全部导入了app。

再来看接下来的两行

app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };

这里定义了apprequestresponse对象,使用了对象的字面量表示法,使其分别继承自req(顶部导入的request.js)和res(顶部导入的response.js),并反向引用了app自身。为什么要这样做呢?这个问题我一开始想不明白,后来我就干脆把这两行代码删了,运行,当然就是报错,答案就在错误中的信息里。

TypeError: Object # has no method ‘send’

显示找不到'send'方法,为什么呢?首先我们从app.get()方法看起,不熟悉的人会找不到它在源码中的位置,其实它在application.js中是这样的

methods.forEach(function(method){
  app[method] = function(path){
    if ('get' == method && 1 == arguments.length) return this.set(path);

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, [].slice.call(arguments, 1));
    return this;
  };
});

methods在顶部模块引入中定义,其实是一个包含各个HTTP请求方法的数组,具体代码在这里
从上面的代码中我们可以看到,这里实际上是遍历了所有methods中定义的方法,当然其中包括get,而且get方法是被’重载’的,即当app.get();的参数只有一个时候,执行的是获取变量的功能,否则,执行route组件中的route.get方法,将该路由和回调函数(即第二个参数)存储进一个栈中(后续会进一步分析)。
回到原来的问题,在这里,关键是看中间的

this.lazyrouter();

我们看它的具体代码

app.lazyrouter = function() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query());
    this._router.use(middleware.init(this));
  }
};

它的作用是在第一次定义路由的时候初始化路由(添加基本的路由),注意最后一句用到了middleware模块的init方法,继续上代码

exports.init = function(app){
  return function expressInit(req, res, next){
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
    req.res = res;
    res.req = req;
    req.next = next;

    req.__proto__ = app.request;
    res.__proto__ = app.response;

    res.locals = res.locals || Object.create(null);

    next();
  };
};

它的作用是初始化requestresponse,可以看到其中用到了我所疑惑app.requestapp.respone,它使reqres继承自了request.jsresponse.js中的定义,也因此在我去掉了那两行代码后会出现res.send找不到的情况。
另外,定义app.response对象时反引用自身,也使得后面在response对象中能够通过this.app获得所创建的express实例。

让我们回到createApplication函数,接下来是app.init();。显然,作用是初始化,做哪些工作呢?

app.init = function(){
  this.cache = {};
  this.settings = {};
  this.engines = {};
  this.defaultConfiguration();
};

设定了cache对象(render的时候用到),各种setting的存储对象,engines对象(模板引擎),最后进行默认的配置,代码有点长这里就不上了,就是做一些默认的配置。

好了,createApplication函数就是这些,当然,其中略去了很多重要的问题,比如路由组件的实现方式,中间件的触发流程等,这我会在后续的总结中进行分析。

最开头的官方示例中还有最后一句

app.listen(3000);

代码如下

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

实际上是调用了Node.js原生的http模块的CreatServer方法,API文档说明是

http.createServer([requestListener])#
Returns a new web server object.

The requestListener is a function which is automatically added to the ‘request’ event.

方法返回的是一个web server对象,其中的参数为HTTP request事件触发后执行的函数(这里我们给的就是我们在createApplication函数中获得的app)。
最后,返回的web server有一个监听端口的listen方法,参数为需要监听的端口号,本示例中即为3000


发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>