Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

express源码解读 #20

Open
SunShinewyf opened this issue Jul 8, 2017 · 1 comment
Open

express源码解读 #20

SunShinewyf opened this issue Jul 8, 2017 · 1 comment

Comments

@SunShinewyf
Copy link
Owner

最近在研究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的源码还是有点复杂的,新手初涉,可能有一些地方理解得不是很到位的,欢迎一起交流

@SunShinewyf
Copy link
Owner Author

SunShinewyf commented Jul 10, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant