koa,redux等主流框架的中间件设计思想原理分析

es6时代来了,相信会让一批有java,C++等面向对象语言开发基础的伙子们,感受到来自js世界满满的善意。es6可以让开发者几乎摆脱prototype的编程模式,让开发更加如丝般顺滑,虽然目前大部分浏览器并没有支持es6,但是打鸡血般突飞猛进的node和与时俱进的babel,还是已经让大部分前端和node开发者享受到es6时代的酸爽。面向对象有很多精妙的设计思想,虽说思想 js框架相信大家都用过不少了,前端如redux,后台框架如express,koa,等等等等,当然还有很多其他优秀的框架,不过与我们今天的主题无关就不多说了。如果大家使用过redux或者koa,应该对其中的中间件不会陌生。中间件虽然在不同的框架中用法各有不同,但是实现原理却是大体一致的。我们发现作为一个中间件,不管其具体实现的是什么能力,其实它一个最主要的职能就是增强目标对象的能力。在研究各大中间件的过程中,隐隐约约看看一个及其熟悉的背影,那就是装饰模式。在众多设计模式中,装饰模式应用最广泛的就是增强目标对象能力,大部分中间件的实现,应该都是借鉴了装饰模式这种灵活的设计思想。因此,这里我们首先来介绍一下 装饰模式,说到装饰模式,就不得不先提一下es7提案中新增的注解功能(本人习惯叫注解,因为写法类似于java中的注解),比如如下一个类,定义了加和减两个方法:

class MyClass {
  add(a, b){
    return a + b;
  }
  sub(a, b){
    return a - b;
  }
}
复制代码

假如现在有个需求,需要实现每次调用add或者sub函数的时候,都分别打印出方法调用前后的log,比如调用前'before operate',调用后打印'after operate',我们是否需要在调用前后分别调用console.log(),es7里面当然不必了,我们只需要定义好我们需要的打印函数,然后使用@注解,比如如下使用方式:

//注解的函数定义
let log = (type) => {
    const logger = console;
    return (target, name, descriptor) => {
      const method = descriptor.value;
      descriptor.value =  (...args) => {
            logger.info(`(${type}) before function execute: ${name}(${args}) = ?`);
            let ret = method.apply(target, args);
            logger.info(`(${type})after function execute: ${name}(${args}) => ${ret}`);
            return ret;
        }
    }
}
//注解调用
class MyClass {
  @log("add")
  add(a, b){
    return a + b;
  }
  @log("sub")
  sub(a, b){
    return a - b;
  }
}
复制代码

如上在我们调用MyClass实例化方法add和sub的时候,分别会打印调用前和调用后的日志了,这就是在不改动MyClass源码的情况下,使用装饰模式对于原方法add和sub的能力增强,这是es7的语法,定义注解的方式很简单,一个函数返回另一个函数,返回函数的参数分别是target:类的上下文,name:目标方法名,descriptor就不用解释了吧,不理解可以看看defineProperty的定义,简单易用,需要增强其他能力,那就多定义几个,多@几下。这是es7的,编译器支持的还是看着有点抽象,接下来我们来看看普通es5对象如何使用装饰模式进行能力的增强。如下一个add函数

function add(a, b){
	return a + b;
}
复制代码

现在需要增强log和notify的能力,在调用前打印日志并发送消息。代码如下:

function logDecorator(target){
	var old = target;
	return function(){
		console.log("log before operate");
		var ret = old.apply(null,arguments);
		console.log(target.name,"results:",ret,",log after operate");
		return ret;
	}
}

function notifyDecorator(target){
	var old = target;
	return function(){
		console.log("notify before operate");
		var ret = old.apply(null,arguments);
		console.log("finished, notify u");
		return ret;
	}
}
var add = logDecorator(notifyDecorator(add));
复制代码

稍微解释一下,var old = target;先将原目标保存,并返回一个函数,在该函数中var ret = old.apply(null,arguments);执行原目标函数的调用,这时候,或前或后,在需要的节点进行具体的能力增强即可,是不是很失望呢,咋就这么简单?不好意思,真就这么简单,这就是各大框架中高大上的中间件的基本原理了。以koa举例,如果我们需要简单实现一个log中间件,应该怎么做呢?

module.exports = (opts = {}) => {
    var log = console.log;
    return async (ctx, next) => {
        log("before ",ctx.request.url, "...");
        await next();
        log("after ",ctx.request.url, "...");
    }
}
复制代码

如上代码就是了,当然,我们可以在中间件中做一些过滤条件,比如我们只希望对非静态资源的请求进行自定义的log等等。koa以及express作为一个后台框架,中间件比较不同的地方就在于路由的实现,听起来似乎有点复杂哦。其实,以koa为例,想要实现路由,我们对ctx.request.url进行字符串分析处理进入不同的处理函数,是否就可以有一个基本的路由功能了呢?所以中间件很强大,其实也很简单,它并不矛盾。中间件定义完了,接下来看看怎么用了。
我们的中间件可能需要十个八个,那这么多个中间件们是如何进行compose呢,不同框架实现方式可能不太一致,但是原理还是同一个原理。一批中间件加入之后,存于一个函数列表中,然后对列表中的函数进行顺序执行,且每一个函数的返回值作为下一个函数的入参。我们以koa和redux的中间件为例来分析一下。首先来看koa的:

app.use(中间件);
复制代码

koa-compose源码:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
复制代码
let fn = compose(middlewares);
fn(ctx)...;
复制代码

首先,使用app.use()加入中间件,使用如上compose函数对中间件middlewares列表进行递归调用。具体代码就不一一解释了吧,对于熟悉koa以及express的同学,应该很熟悉next的用法,这其实就是我们前面的var old = target;这种方式的升级版本,并且通过next的方式可以更加优雅地解了中间件新增的问题,而不需要使用嵌套调用的方式。
递归遍历是个思路,其实我们js原生提供了一种方式进行compose,可以更加优雅解决这个问题,redux就是采用了这种调用方式,就是使用reduce函数,我们来看看redux处理方式:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码

reduce再加上es6简直赏心悦目有没有,如果看的不太舒服可以转成es5看看,给大家一个简单的测试用例跑跑,可能会更加好理解:

function fun1(obj){
	console.log(1);
	obj.a=1;
	return obj;
}
function fun2(obj){
	console.log(2);
	obj.b=2;
	return obj;
}
let fn = compose(fun1,fun2);
fun({});
复制代码

看看调用的结果是啥,这只是一个帮助理解的小栗子,栗子虽小,但是已经小秀了一把肌肉了,重点就在于我们在各个中间件中透传传入的这个参数obj了,可以是个对象,也可以是个函数,总之是我们可以为所欲为地增强它的能力。
根据不同的目的,中间件的实现机制会有一些差异,koa跟redux其实就有比较明显的一些区别,有兴趣可以深入去看看,但是万变不离其宗。
到此,中间件的定义和调用中的一些核心逻辑就讲完了,都是个人一些浅见,水平有限,如有谬误,敬请指出!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值