SunShinewyf/issue-blog

express源码解读

SunShinewyf opened this issue · 1 comments

最近在研究experss,在造过一个简单的轮子之后,想通过研究一下它的源码来了解一下内部的实现原理,如有不对的地方希望得到大家的指正。

研究的express版本为4.15.0,应该算是比较新的版本了。

文件结构及内容

- 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

上图中的目录结构比较清晰,router目录中主要是router路由的功能,middleware目录是中间件的一些功能。比较重要的是express.jsapplication.js还有router文件夹中的文件。express.js中很简单,只是暴露了一个工厂函数createApplication,这个函数虽然简洁,但是却完成了生成一个完成app的整个过程,具体代码如下:

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

application.js中,可以看到app.listen的实现:

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

由此就可以知道使用express实例化一个项目,其实和使用原生node创建一个应用的步骤差不多,只是express进行了一层封装而已。
application.js这个文件主要是暴露了express内部的一些api,比如app.render(),app.param(),app.set()等。

路由

expressrouter中总共包含了三个文件,index.js,route.jslayer.js,并且分别定义了router,route,layer三个构造函数,其中routerroute这两个对象都包含stack这个成员属性,也就是中间件数组。并且routerroute里面的stack也不一样,如下图:

images

如上图可见,router中的stack数组是由route对象组成的,而route中的stack则是由layer数组组成的。三者关系可以描述成下图:

images

route对象代表的是路由对象,每一条路由都会实例化一个route对象,而router则是一个路由集合,在上一篇中曾提到router是一个“微型应用程序”,这说明routerroute之间的关系是一个包含与被包含的关系,前者的功能更强大,后者只是处理单条路由的一些功能。
路由的整个功能和逻辑顺序大概如下:
在触发一个路由的时候,比如访问/login时,会执行router的成员函数handle(),在这个函数中会将传进来的requrl做一些处理,比如获取url里面的参数,并会遍历stack数组中的每一个layer,遍历主要是通过next()实现的,这个next()主要是非路由中间件的next()函数,具体代码如下:

  function next(err) {
    ....//省略了一些代码
    
    // 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;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        // process non-route handlers normally
        continue;
      }
         ....//省略一些源码
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }

    // store route for dispatch on change
    if (route) {
      req.route = route;
    }
     .....
    // 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);
    });
  }

遍历过程中会执行一个match(),如果匹配成功(断点发现当stack.name=router的时候会匹配成功),则会去执行route中的dispatch方法,则获取当前routestack数组并通过next()再进行遍历一次。当再次匹配成功之后,就会去调用layer对象的handle_request()方法。也就是调用中间件函数,这个函数的代码如下:

  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }

上面也是执行中间件的主要部分,首先是判断路由函数中传进来的回调函数的参数个数,若大于3,则属于路由中间件,则首先处理next(),这里的next()函数代码如下(定义在route.js文件中:

 function next(err) {
    if (err && err === 'route') {
      return done();
    }
    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }
    if (layer.method && layer.method !== method) {
      return next(err);
    }
    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

这个next()主要是处理路由中间件的,不断遍历,使得中间件一个个执行。对于路由中间件和非路由中间件,两者也是有区别的,路由中间件的定义是在router/index.js中的route原型方法中,具体代码如下:

proto.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属性

  this.stack.push(layer);
  return route;
};

从上面代码可以看出,路由中间件中的route不为空值,且为一个route对象,并且该对象中有methods的一个成员属性,用来定义一些路由方法。再来看看非路由中间件的定义方法,放在了router/index.js中的use原型方法中,具体代码如下:

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';
  ....//省略部分源码
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined; //其中的route属性为undefined
    this.stack.push(layer);
  }

  return this;
};

从上面源码可以看出,非路由中间件中的route属性值为undefined,当使用app.use()时其实也就是触发了router.use()方法。

动态添加method

源码中并没有直接遍历路由中的每一种方法,比如get,post,put,而是动态添加,不论是最后导出的app示例,还是route中对方法的处理,都是使用的这种方式,具体代码如下:

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

route对象中,同样定义了这样一个处理method的操作:

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 callback functions 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;
  };
});

这种方法比较灵活,不需要将所有的method遍历一遍。

总结

express的源码还是有点复杂的,新手初涉,可能有一些地方理解得不是很到位的,欢迎一起交流