资讯专栏INFORMATION COLUMN

express 的 middleware 设计

zollero / 2062人阅读

摘要:入口文件在文件夹下的,其向外界暴露了一些方法。方法也是从中继承的。入口文件很清晰,主要是完成方法的暴露以及的一些初始化操作。下一篇写写路由的实现。

还没用express写过server,先把部分源码撸了一遍,各位大神求轻拍。

express入口文件在lib文件夹下的express.js,其向外界暴露了一些方法。

最主要的(express.js 第36-47行):

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);         //各中间件的处理入口,handle方法通过mixin拓展于proto
  };

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

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

exports = module.exports = createApplication;

我们经常在自己的业务代码中这样写:

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

其实就是调用createApplication方法.这样就实例化了一个app。这个app比较特殊,通过mixin集成了一些其他的属性

mixin(app, EventEmitter.prototype, false); //拓展了事件发射器原型对象
mixin(app, proto, false); //拓展了application.js中的属性和方法

在我们业务代码实例化app的时候,调用了app.init()方法完成了一些初始化的配置。init()方法也是从application.js中继承的。

入口文件很清晰,主要是完成方法的暴露以及app的一些初始化操作。

接下来看下application.js中的部分代码逻辑:

第136-146行,延迟实例化一个_router

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

    this._router.use(query(this.get("query parser fn")));
    this._router.use(middleware.init(this));
  }
};

第157-174行,这是各个middleware的入口,

app.handle = function handle(req, res, callback) {
  var router = this._router;    //获取已经实例化得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);    //当http过来时,对于request和response的处理从这个地方开始
};

第187-242行,app.use方法提供了应用级的middleware,但是事实上在214行,this.lazyrouter()新建一个route,第219-221行,然后根据app.use(fn)传入的参数挂载到了route.use()路由级中间件上了。app.use()route.use的一个代理。

app.use = function use(fn) {
  var offset = 0;
  var path = "/";

  // default path to "/"
  // disambiguate app.use([fn])
  if (typeof fn !== "function") {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== "function") {
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset)); //铺平arguments

  if (fns.length === 0) {
    throw new TypeError("app.use() requires middleware functions");
  }

  // setup router
  this.lazyrouter();                                //如果没有route实例则新建一个
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app                              //如果传入的不是express实例app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);                  //将中间件注入到router中
    }

    debug(".use app under %s", path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) { 
      var orig = req.app;
      fn.handle(req, res, function (err) {          
        req.__proto__ = orig.request;
        res.__proto__ = orig.response;
        next(err);
      });
    });

    // mounted an app
    fn.emit("mount", this);
  }, this);

  return this;
};

第255-258行,代理到router实例的route()的方法中:

    app.route = function route(path) {
        this.lazyrouter();
        return this._router.route(path);
    };

router实例是通过router/index.js提供构造函数来创建的,在这个文件夹中第42-60行:

var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);    //handle方法继承于proto
  }

  // mixin Router class functions
  router.__proto__ = proto;

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];        //初始化一个stack.这个stack中保存了注册的所有中间件

  return router;
};

提供了一个router的构造函数,它的原型对象上,第136行提供了proto.handle方法,这个方法的作用就是接收来自httpreqres

第413行,提供了proto.use方法,正如上面所说的app.useroute.use的代理,这个方法的特殊性就在任何的http请求都会经过在app.use上挂载的中间件,例如现在express4.x已经将很多中间件从自身移除,需要你重新通过npm去安装,然后在业务代码中进行引用,例如使用body-parser中间件:

var express = require("express");
var app = express();
var bodyParser = require("body-parser");
app.use(bodyParser.json());

这样每次http请求过来的时候首先会经过bodyParser.json()这个中间件,它提供了一个向req添加req.body = {}方法,并传向下一个中间件的作用。
同时在route.use内部,第439-458行,

for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    if (typeof fn !== "function") {
      throw new TypeError("Router.use() requires middleware function but got a " + gettype(fn));
    }

    // add the middleware
    debug("use %s %s", path, fn.name || "");

    var layer = new Layer(path, {       //新建一个layer,layer上挂载了error_handler和request_handler
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;            

    this.stack.push(layer);             //route自身会维护一个stack,将每个新建的layer都推入stack当中,这个layer实例最终会对匹配的path,作出error_handle或者request_handle。
  }

第477行,proto.route方法提供了一个新建route的方法。

proto.route = function route(path) {
  var route = new Route(path);      //新建一个route,这个Route构建函数内部实现见./route.js,它里面提供了一个空的stack,用以

  var layer = new Layer(path, {                     //新建一个layer,layer的作用见下面的讲解
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route;  

  this.stack.push(layer);   //新建一个route,这个route会维护自身的stack
  return route;
};

var route = require("express").Router(),但是这个方法不同的地方
在于,它会自身维护一个stack,这个stack中有你在这个方法上面定义的所有中间件。同样,你可以通过这个route挂载对于不同路径的req, res的处理。

使用的方法:

var express = require("express");
var app = express();
var router = express.Router();

//没有挂载任何路径的中间件,通过该路由的每个请求都会执行该中间件
router.use(function(req, res, next) {
    console.log("route.use");
})


router.get("/test", function(req, res, next) {
    console.log("route.get"); 
});

//最后需要将这个router挂载到应用
app.use("/", router);

以上部分主要是整个express的中间件的挂载。总结一下:

通过app.use()挂载的中间件最终都代理到了router.use()方法下

router.use()方法,新建一个layer,layer上保存了路径,默认为"/",及相应的处理方法,并存入这个app维护的stack中。

通过var router = require("express").Router()新建的router路径级实例,同样可以挂载不同的中间件,不过最后需要将这个router路由注入到app应用当中:app.use("/", router);

接下来讲下当http请求到来的时候,数据的流向: 在你定义中间件的过程中,因为是维护了一个app或者route实例,它们分别都有一个stack。这个stackFIFO的,因此每当一个请求过来的时候,数据从最开始的定义的中间件开始,一直向下按顺序进行传递,因此你可以自己定义,当然,你需要调用next()方法。就比如Route.protoype.dispath方法

//将req, res分发给这个route
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if (stack.length === 0) {
    return done();
  }

  var method = req.method.toLowerCase();
  if (method === "head" && !this.methods["head"]) {
    method = "get";
  }

  req.route = this;

  next();

  function next(err) {
    if (err && err === "route") {
      return done();
    }

    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }

    if (layer.method && layer.method !== method) {  //匹配传入的req请求方式,和layer的method进行对比
      return next(err);
    }

//调用layer.handle,用以错误处理或者request处理
    if (err) {
      layer.handle_error(err, req, res, next);      
    } else {
      layer.handle_request(req, res, next);         
    }
  }
};

最后,http请求的处理:
app或者route实例中,自身有一个stack,这个stack就存放了在挂载中间时新建的layer,每个layer实例都保存了对应的路径,以及相应的error_handlerequest_handle

谢谢大家看到这里,欢迎大家斧正。

下一篇写写express路由的实现。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80143.html

相关文章

  • [译]Express应用结构最佳实践

    摘要:为应用增加新的特性和处理新的情况可能都会改变文件的结构。写一个模板的最佳实践是,不要在模板中处理数据。在上面这四个文件夹中,主要的测试代码将是单元测试,这意味着你需要将被测试的代码与应用分离开来。 前言 Node和Express并不严格要求它的应用的文件结构。你可以以任意的结构来组织你的web应用。这对于小应用来说,通常是不错的,十分易于学习和实验。 但是,当你的应用在体积和复杂性上都...

    dreamans 评论0 收藏0
  • Web框架常用架构模式(JavaScript语言)

    摘要:只能在不同的时候选用不同的假设和不同的理论来解释问题,许来西的文章讲到科学一定程度上通过放弃一贯性换取了实用性,放弃自洽性换取了它洽性。然而遗憾的是本身只提供了模块和洋葱模型的最小封装。 在写干货之前,我想先探(qiang)讨(diao)两个问题,模式的局限性?模式有什么用? 最近看到一篇文章对我启发很大,许来西在知乎的回答《哲学和科学有什么关联?》,全篇较长,这里摘录我要引出的一点:...

    loostudy 评论0 收藏0
  • 通过nodeclub项目源码来讲解如何做一个nodejs + express + mongodb项目

    摘要:是的源码,算是一个基本的博客系统,包含文章发布,关注,评论等功能。这些功能可以说是任何一个网站的基础。比如运营数据配置和其他数据配置分开,因为很有可能需要做一个小的工具来让非技术人员配置相关参数。模式在中有一个专门的章节来讲解。 1. About 1.1 what: nodeclub是cnodejs.com的源码,cnode算是一个基本的博客系统,包含文章发布, 关注,评论等功能...

    kaka 评论0 收藏0
  • Express到Nestjs,谈谈Nestjs设计思想和使用方法

    摘要:三的洋葱模型这里简单讲讲在中是如何分层的,也就是说请求到达服务端后如何层层处理,直到响应请求并将结果返回客户端。从而使得端支持跨域等。   最近已经使用过一段时间的nestjs,让人写着有一种java spring的感觉,nestjs可以使用express的所有中间件,此外完美的支持typescript,与数据库关系映射typeorm配合使用可以快速的编写一个接口网关。本文会介绍一下作...

    chanjarster 评论0 收藏0
  • Express到Nestjs,谈谈Nestjs设计思想和使用方法

    摘要:三的洋葱模型这里简单讲讲在中是如何分层的,也就是说请求到达服务端后如何层层处理,直到响应请求并将结果返回客户端。从而使得端支持跨域等。   最近已经使用过一段时间的nestjs,让人写着有一种java spring的感觉,nestjs可以使用express的所有中间件,此外完美的支持typescript,与数据库关系映射typeorm配合使用可以快速的编写一个接口网关。本文会介绍一下作...

    wawor4827 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<