从 express 和 koa 理解中间件

740 阅读2分钟

简介

个人理解的中间件:中间件的本质就是一个函数,在收到请求和返回相应的过程中做一些我们想做的事情。  express 官方: 

  1. 执行任何代码。
  2. 修改请求和响应对象。
  3. 终结请求-响应循环。
  4. 调用堆栈中的下一个中间件。

express 中间件

中间件比较出彩的应用在 Koa,Redux,Express 中,我的偶像阿宝哥有写一篇从 Koa 更好的理解中间件和洋葱模型的文章,所以这次我打算主要从 Express 来理解中间件。 首先我们下载 express 源码 ,我们可以通过命令行下载

$ npm install express --save

express 中间件的类别

  1. 应用级中间件
  2. 路由级中间件
  3. 错误处理中间件
  4. 内置中间件
  5. 第三方中间件

express 中间件的初使用

我们来看一个简单的使用示例,这个例子监听了3000端口,我们通过 use 方法,给它增加了3个中间件。

var express = require("express");

var app = express();
app.listen(3000, function () {
  console.log("listen 3000...");
});

function middlewareA(req, res, next) {
  console.log("middlewareA before next()");
  next();
  console.log("middlewareA after next()");
}

function middlewareB(req, res, next) {
  console.log("middlewareB before next()");
  next();
  console.log("middlewareB after next()");
}

function middlewareC(req, res, next) {
  console.log("middlewareC before next()");
  next();
  console.log("middlewareC after next()");
}

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

运行结果 中间件.png 我们发现执行完一个中间件,走到 next() 后,会跳过之后语句,执行下一个中间件,当所有执行完毕后,又会反向的执行 next() 之后的语句。 中间件流程.png 通过上图,我们可以很清晰的看到中间件是什么,可以应用在哪些场景。通过中间件,我们可以精确的控制某个执行操作的前置和后置操作,例如 axios 发送请求前的处理,接收请求后的处理。 那么 express 是如何做到的这种任务执行方式的呢?

express 中间件的实现原理

首先我们先输出这样的一个概念:Layer (中间件)实例是 path 和 handle 互相映射的实体,每一个 **Layer **便是一个中间件。 然后我们看一下 express 中间件的初始化 中间件.png 通过上图我们发现,中间件中间可能嵌套中间件,面对这种情况,express 就在 Layer 中做了处理,我们分两种情况挂载中间件。

  1. 通过使用app.userouter.use 来挂载。
    1. app.use 经过处理以后,最终也是调用 router.use
  2. 使用 app.[Http Method]app.routerouter.allrouter.[Http Method]router.route来挂载的。
    1. app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]经过一系列处理之后最终也是调用 router.route 的。

所以我们主要研究 router.userouter.route

router.use

我们在 router/index.js 中看见定义

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';
  if (typeof fn !== 'function') {
   		......
    }
  if (typeof arg !== 'function') {
			......
    }
  }
  var callbacks = flatten(slice.call(arguments, offset));
  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
    if (typeof fn !== 'function') {
      ......
    }
    debug('use %o %s', path, fn.name || '<anonymous>')
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
    layer.route = undefined;
    this.stack.push(layer);
  }
  return this;
};

小结:没有深层嵌套的中间件,会使用 router.use 进行注册。

router.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;
  this.stack.push(layer);
  return route;
};

小结:没有深层嵌套的中间件,会使用 router.route进行注册。 我们发现不管是哪种方法,最后都会往 stack数组中添加我们的中间件。 当事件触发后 app.handle 会对 stack 数组进行处理

app.handle = function(req, res, callback) {
	var stack = this.stack;
	var idx = 0;
	function next(err) {
		if (idx >= stack.length) {
		  callback('err') 
		  return;
		}
		var mid;
		while(idx < stack.length) {
		  mid = stack[idx++];
		  mid(req, res, next);
		}
	}
	next()
}

通过尾递归调用,next 方法不断取出 stack 中的中间件进行调用,并且将自身传递给中间件作为第三个参数,每个中间件的约定固定格式为 (req, res, next) =>{},这样每个中间件函数只要调用了 next 方法,即可调用下一个中间件,关于尾调用有兴趣的可以阅读下阮老师的尾调用优化。 关于next()后是如何触发,还没搞清楚。 整体流程就是中间件注册后添加进 stack 数组中,当触发时,进行递归调用, 一个触发一个

koa 里的中间件

koa 相比 express 而言,代码更加高级精炼,代码基于 ES6 实现,支持 generator(async, await) 没有内置路由实现和任何内置中间件, context 的设计也很是巧妙。

中间件的处理

  1. 构造函数 constructor 中维护全局中间件数组 this.middleware和全局的this.context 实例(源码中还有request,response对象和一些其他辅助属性)。
  2. express 不同,因为没有router的实现,所有this.middleware 中就是普通的”中间件“函数而非复杂的 layer 实例

koa 中的任务编排

koa中任务执行的顺序是通过把前置执行和后置执行,放在 await next() 的前后完成任务编排,应用了 async await 的执行顺序的原理。

// 统计请求处理时长的中间件
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

koa 中的任务调度

通过之前了解,我们知道中间件统一放在 middleware 数组中保存,为了完成任务,我们就要将他一个一个拿出来执行,相关的调度算法在 koa-compose 下的compose 函数中。

function compose(middleware) {
  // 省略部分代码
  return function (context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

看起来和 express 的 next 有点像,但是因为支持异步,所以使用的是 promise 形式的,中间件async(ctx, next) =>{}每一个执行都是返回一个 promise ,第二个参数的值dispatch.bind(null, i+1),所以中间件会一个一个向下执行,当最后一个执行完毕,resolve掉,然后开始执行 await 队列中的方法,最终使最外层的 promise resolve掉。 小结: 这里和 express 最大的不同点就是 koa 的响应处理是在中间件执行完,返回 promise resolve后。 我们看一下 koa 的 application 中的部分代码

// application.js 中部分代码
constructor() {
	super()
	this.middleware = []
	this.context = Object.create(context)
}

use(fn) {
	this.middleware.push(fn)
}

listen(...args) {
	debug('listen')
	const server = http.createServer(this.callback());
	return server.listen(...args);
}

callback() {
	// 这里即中间件处理代码
	const fn = compose(this.middleware);
	
	const handleRequest = (req, res) => {
	  // ctx 是koa的精髓之一, req, res上的很多方法代理到了ctx上, 基于 ctx 很多问题处理更加方便
	  const ctx = this.createContext(req, res);
	  return this.handleRequest(ctx, fn);
	};
	
	return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
	ctx.statusCode = 404;
	const onerror = err => ctx.onerror(err);
	const handleResponse = () => respond(ctx);
	return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

我们发现,监听触发以后,会执行 callback ,这里会处理中间件代码,处理完毕以后会执行 handleRequest,通过 handleResponse 最后对响应做处理,”中间件“会设置ctx.bodyhandleResponse也会主要处理 ctx.body ,所以 koa 的”洋葱圈“模型才会成立,await next()后的代码也会影响到最后的响应。

洋葱模型

洋葱.png 小结:我们通过express 和 koa 理解了如何实现一个洋葱模型的任务编排和调度。