express中间件,了解一下

本篇文章从express源码上来理解中间件,同时可以对express有更深层的理解

前言

中间件函数可以执行哪些任务?

  • 执行任何代码。
  • 对请求和响应对象进行更改。
  • 结束请求/响应循环。
  • 调用堆栈中的下一个中间件函数。

我们从一个app.use开始,逐步分析到下一个中间件函数的执行。

初始化服务器

首先从github上下载express源码。

建立一个文件test.js文件,引入根目录的index.js文件,实例化express,启动服务器。


let express = require('../index.js');
let app = express()

function middlewareA(req, res, next) {
  console.log('A1');
  next();
  console.log('A2');
}

function middlewareB(req, res, next) {
  console.log('B1');
  next();
  console.log('B2');
}

function middlewareC(req, res, next) {
  console.log('C1');
  next();
  console.log('C2');
}

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

app.listen(8888, () => {
  console.log("服务器已经启动访问http://127.0.0.1:8888");
})

复制代码

启动服务器,通过访问http://127.0.0.1:8888服务,打开终端,看看终端日志执行顺序。

从日志我们可以看出,每次next()之后,都会按照顺序依次调用下中间件函数,然后按照执行顺序依次打印A1,B1,C1,此时中间件已经调用完成,再依次打印C2,B2,A2

目录结构

--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

复制代码

通过实例化的express,我们可以看到,index.js文件实际上是暴露出lib/express的文件。

实例化express

express,通过mixin继承appLication,同时初始化application。


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

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, 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;
}

复制代码

而mixin是merge-descriptorsnpm模块。Merge objects using descriptors.

打开application.js文件,发现express的实例化源自var app = exports = module.exports = {}
进一步搜索app.use,找到app.use,而app.use又只是向应用程序路由器添加中间件的Proxy


/**
 * Proxy `Router#use()` to add middleware to the app router.
 * See Router#use() documentation for details.
 *
 * If the _fn_ parameter is an express app, then it will be
 * mounted at the _route_ specified.
 *
 * @public
 */

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

  // 默认path 为 '/'
  // app.use([fn])
  //判断app.use传进来的是否是函数
  if (typeof fn !== 'function') {
    var arg = fn;

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

    // 第一个参数是路径
    //取出第一个参数,将第一个参数赋值给path。
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }
  //slice.call(arguments,offset),通过slice转为数据,slice可以改变具有length的类数组。
  //arguments是一个类数组对象。
  
  //处理多种中间件使用方式。
  // app.use(r1, r2);
  // app.use('/', [r1, r2]);
  // app.use(mw1, [mw2, r1, r2], subApp);
  
  var fns = flatten(slice.call(arguments, offset));//[funtion]

  //抛出错误
  if (fns.length === 0) {
    throw new TypeError('app.use() requires a middleware function')
  }

  //设置router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // 处理不是express的APP应用的情况,直接调用route.use。
    if (!fn || !fn.handle || !fn.set) {
    //path default to '/'
      return router.use(path, fn);
    }
    
    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

    // app mounted 触发emit
    fn.emit('mount', this);
  }, this);

  return this;
};


复制代码

定义默认参数offerpath。然后处理fn形参不同类型的情况。将不同类型的中间件使用方式的形参转为扁平化数组,赋值给fns

forEach遍历fns,判断如果fnfn.handlefn.set参数不存在,return出去router.use(path, fn)

否则继续执行router.use

调用handle函数,执行中间件。

代码如下:

/**
*将一个req, res对分派到应用程序中。中间件执行开始。
*如果没有提供回调,则默认错误处理程序将作出响应
*在堆栈中冒泡出现错误时。
*/
app.handle = function handle(req, res, callback) {
  var router = this._router;

  // 最后报错处理error。
  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);
};

复制代码

惰性添加Router。

从上述代码中可以知道,app.use的作用实际上是将各种应用函数传递给router的一个中间层代理。

而且,在app.use中有调用this.lazyrouter()函数,惰性的添加默认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')));
    //初始化router
    this._router.use(middleware.init(this));
  }
};

复制代码

这里对Router进行了实例化,同时设置基本的optioncaseSensitive 是否区分大小写,strict 是否设置严格模式。

Router初始化如下:

/**
 * 用给定的“选项”初始化一个新的“路由器”。
 * 
 * @param {Object} [options] [{ caseSensitive: false, strict: false }]
 * @return {Router} which is an callable function
 * @public
 */

var proto = module.exports = function(options) {

  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // 混合路由器类函数
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

复制代码

调用app.use时,参数都会传递给router.use,因此,打开router/index.js文件,查找router.use


/**
*使用给定的中间件函数,具有可选路径,默认为“/”。
* Use(如' .all ')将用于任何http方法,但不会添加
*这些方法的处理程序,所以选项请求不会考虑“。use”
*函数,即使它们可以响应。
*另一个区别是_route_ path被剥离,不可见
*到处理程序函数。这个特性的主要作用是安装
*无论“前缀”是什么,处理程序都可以在不更改任何代码的情况下操作
*路径名。
 *
 * @public
 */

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // 默认路径  '/'
  // 消除歧义 router.use([fn])
  // 判断是否是函数
  if (typeof fn !== 'function') {
    var arg = fn;
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }
    // 第一个参数是函数
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  //将arguments转为数组,然后扁平化多维数组
  var callbacks = flatten(slice.call(arguments, offset));

  //如果callbacks内没有传递函数,抛错
  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  //循环callbacks数组
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
    
    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    //解析下query和expressInit的含义
    // 添加中间件
    //匿名 anonymous 函数
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive, //敏感区分大小写 //默认为false
      strict: false, //严格
      end: false //结束
    }, fn);

    layer.route = undefined;
    this.stack.push(layer);
  }

  return this;
}

复制代码

router.use的主要作用就是将从app.use中传递过来的函数,通过Layer实例化的处理,添加一些处理错误处理请求的方法,以便后续调用处理。同时将传递过来的path,通过path-to-regexp模块把路径转为正则表达式(this.regexp),调用this.regexp.exec(path),将参数提取出来。

Layer代码较多,这里不贴代码了,可以参考express/lib/router/layer.js

处理中间件。

处理中间件就是将放入this,stacknew Layout([options],fn),拿出来依次执行。

proto.handle = function handle(req, res, out) {
  var self = this;

  debug('dispatching %s %s', req.method, req.url);

  var idx = 0;
  //获取协议与URL地址
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  //是否添加斜杠
  var slashAdded = false;
  var paramcalled = {};

  //存储选项请求的选项
  //仅在选项请求时使用
  var options = [];

  // 中间件和路由
  var stack = self.stack;
  // 管理inter-router变量
  //req.params 请求参数
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // 设置下一层
  req.next = next;

  // 对于选项请求,如果没有其他响应,则使用默认响应
  if (req.method === 'OPTIONS') {
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }

  // 设置基本的req值
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

  next();

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    //是否添加斜线 默认false
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // 恢复改变req.url
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // 出口路由器信号
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // 不再匹配图层
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // 获取路径pathname
    var path = getPathname(req);

    if (path == null) {
      return done(layerError);
    }

    // 找到下一个匹配层
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      //try layer.match(path) catch err
      //搜索 path matchLayer有两种状态一种是boolean,一种是string。
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        layerError = layerError || match;
      }

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

      if (!route) {
        //正常处理非路由处理程序
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      var method = req.method;
      var has_method = route._handles_method(method);

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
      }
    }

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

    //重新赋值router。
    if (route) {
      req.route = route;
    }

    // 合并参数
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
      
    var layerPath = layer.path;

    // 处理参数
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      if (route) {
        return layer.handle_request(req, res, next);
      }

    // 处理req.url和layerPath,同时对layer中的请求error和handle_error加tryCatch处理。
      trim_prefix(layer, layerError, layerPath, path);
    });
  }

复制代码

执行proto.handle中间件也就的while循环中的一些核心代码,每次调用app.use中的回调函数中的next()都会让idx加一,将stack[idx++];赋值给layer,调用一开始说到的layer.handle_request,然后调用trim_prefix(layer, layerError, layerPath, path),添加一些报错处理。

trim_prefix函数如下:


 function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      // Validate path breaks on a path separator
      var c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') return next(layerError)

      // //删除url中与路由匹配的部分
      // middleware (.use stuff) needs to have the path stripped
      debug('trim prefix (%s) from url %s', layerPath, req.url);
      removed = layerPath;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // Ensure leading slash
      if (!protohost && req.url[0] !== '/') {
        req.url = '/' + req.url;
        slashAdded = true;
      }

      // 设置 base URL (no trailing slash)
      req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
        ? removed.substring(0, removed.length - 1)
        : removed);
    }

    debug('%s %s : %s', layer.name, layerPath, req.originalUrl);

    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

复制代码

总结

以上就是通过app.use调用之后,一步步执行中间件函数router.handle

next核心代码很简单,但是需要考虑的场景却是很多,通过这次源码阅读,更能进一步的理解express的核心功能。

虽说平常做项目用到express框架很少,或者可以说基本不用,一般都是用Koa或者Egg,可以说基本上些规模的场景的项目用的都是Egg

但是不可否认得是,express框架还是一款非常经典的框架。

以上代码纯属个人理解,如有不合适的地方,望在评论区留言。

猜你喜欢

转载自juejin.im/post/5c10b5176fb9a049af6d1e1e