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其实就有比较明显的一些区别,有兴趣可以深入去看看,但是万变不离其宗。
到此,中间件的定义和调用中的一些核心逻辑就讲完了,都是个人一些浅见,水平有限,如有谬误,敬请指出!!!