Middleware(中间件)之道

96
CatchZeng
0.2 2018.09.26 13:01 字数 1655

一、前言

在日常开发中,我们经常遇到逻辑复杂的业务,导致代码写得又长又乱。有些逻辑像一个流程,在不同的节点需要做不同的操作。
比如,我们经常会遇到上传文件的业务。该业务要求先验证文件正确性,然后上传,最后跳转到成功的页面。

if  checkFile {
    uploadFile  { result
            if result {
                  showSuccessView { result
                             if result {
                                  //handle success 
                             } else {
                                  //handle error
                              }  
                  }        
            } else {
               //TODO handle error
            }
    }     
} else {
  //TODO handle error
}

我们可以看到伪代码中呈现着回调地狱,上传前、上传、上传后的操作逻辑也零散地分布,很容易造成阅读和维护困难,而中间件的出现,让我们处理这类业务变得简单很多。

在讲中间件之前我们先来回顾下AOP。

二、AOP

AOP意为面向切面编程。 以页面统计为例,先来看下传统的流程。

页面统计

我们可以把方框里的流程合为一个,另外系统还会有其他页面统计流程,我们先把这些流程放到一起:

页面统计

不难发现每个页面都有一个相同的页面统计流程。这样的处理有如下几个问题:

  • 重复代码:每个界面都需要加入页面统计的代码
  • 影响主流程的清晰度:页面统计跟主流程无关,但又需要加入到各个界面中
  • 扩展性差:新增一个界面,就得为其加入页面统计的代码

有没有想过把这个页面统计的代码是提取出来,不放到主流程里去呢?这就是AOP的思想了,传统的流程讲究从上而下的处理流程 ,而AOP讲究“面”,从横向切面将相同的流程提取出去,所以也叫“横切面”,如下图所示。

Group 5.png

AOP提倡从横向切面思路向管道某个位置插入一段代码逻辑,这样就实现在任何业务逻辑前后都有相同代码逻辑段,开发者只需专注写业务逻辑,既不影响主流程,而且隔离了业务逻辑,达到高内聚低耦合

附上iOS使用AOP实现页面统计的代码,帮助大家理解AOP。

@implementation UIViewController (Analytics)

+ (void)load{
    [self swizzleInstanceMethod:@selector(viewWillAppear:) with:@selector(swizzled_viewWillAppear:)];
    [self swizzleInstanceMethod:@selector(viewWillDisappear:) with:@selector(swizzled_viewWillDisappear:)];
}

- (void)swizzled_viewWillAppear:(BOOL)animated{
    [self swizzled_viewWillAppear:animated];
    [Analytics beginTimingEvent: self.className];
}

- (void)swizzled_viewWillDisappear:(BOOL)animated{
    [self swizzled_viewWillDisappear:animated];
    [Analytics endTimingEvent: self.className];
}

思考

  • AOP和OOP是什么关系?有什么区别?
    写多了OOP的代码,会发现AOP跟OOP的思路不同。OOP是将做同一件事情的业务逻辑封装成一个对象。但是,在做一件事情过程(主流程)中又想做别的事情(比如页面统计)对OOP来说难以解决。而AOP的出现让OOP代码能专注于主流程,更好地遵循单一职责原则,提高内聚性。所以,我认为AOP对OOP做了一个补充。
  • 怎么判断代码是否达到了高内聚?
    这个问题吊一下大家胃口,这里先不解答,大家可以在留言处评论,后续会贴出个人的理解。

  • AOP思想如何解决上传文件的业务的问题?
    由于JavaScript的动态性较好,下面以JavaScript代码为例,看下如何利用AOP优化上传文件业务。

利用AOP优化上传文件业务

AOP从横向切面思路向管道某个位置插入一段代码逻辑。而这个位置通常就是调用前(before)和调用后(after)。

Function.prototype.before = function(fn){
  var self = this;  
   return function(){  
     var res = fn.call(this);  
     if(res) {
        self.apply(this, arguments); 
     }
   }  
}  

Function.prototype.after = function(fn){
  var self = this;  
   return function(){  
     self.apply(this, arguments);  
     fn.call(this);  
   }  
} 

有了这两个扩展,可以按下面的方式实现上传业务。

function checkFile(){  
   console.log('checking file');  
   if fileIsVaild() {
      return true
   } else {
      console.log('file is invalid');  
      return false
    }
}

function uploadFile() {
  console.log('uploading file');  
  if fileUploadSuccess() {
      return true
   } else {
      console.log('file upload failed');  
      return false
   }
}

function showSuccessView() {
  console.log('show success view');  
}

uploadFile.before(checkFile).after(showSuccessView)();

思考

  • 使用AOP后解决了什么问题?还有什么问题?
    从上面的例子,可以看出AOP已经实现了业务隔离。但却带来了一串长长的链式调用,如果处理不当很容易掉链子。另外,这种结构实现异步操作较为麻烦。

  • 有什么办法,既能隔离业务,又能清爽地使用?
    这时候,我们的主角就该上场了。

三、中间件

为了理解中间件,我们来看下Koa的中间件使用。

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}
app.use(logger);

像上面代码中的logger函数就叫做"中间件"(middleware),因为它处在 HTTP Request 和 HTTP Response 中间,用来实现某种中间功能。app.use()用来加载中间件。

中间件栈

多个中间件会形成一个栈结构(middle stack),以"先进后出"(first-in-last-out)的顺序执行,被称为洋葱结构

洋葱结构

举个例子

const one = (ctx, next) => {
  console.log('>> one');
  next();
  console.log('<< one');
}

const two = (ctx, next) => {
  console.log('>> two');
  next(); 
  console.log('<< two');
}

const three = (ctx, next) => {
  console.log('>> three');
  next();
  console.log('<< three');
}

app.use(one);
app.use(two);
app.use(three);

/*result
>> one
>> two
>> three
<< three
<< two
<< one
*/

中间件一个奇妙的点在于next函数。如果中间件内部没有调用next函数,那么执行权就不会传递下去。

中间件的实现

export default class MiddlewareCenter {
    constructor() {
        this._middlewares = []
        this._context = null
    }

    use(middleware) {
        if (typeof middleware != 'function') {
            console.warn('middleware must be a function.')
            return null
        }
        this._middlewares.push(middleware)
        return this
    }

    handleRequest(context) {
        const fn = compose(this._middlewares)
        this._context = context
        fn(this._context)
    }
}

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, dispatch.bind(null, i + 1)))
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

详细的代码可以看我的开源项目middleware-center

利用中间件优化上传业务

const middlewareCenter = new UploadFileCenter()
middlewareCenter.handle('uploader')

/* result
beforeUpload
startUplaod
finishUpload
after finishUpload
after startUplaod
after beforeUpload
*/
class UploadFileCenter extends MiddlewareCenter {

    constructor() {
        super()
        this._middlewareMap = { 'uploader': [this.beforeUpload, this.startUplaod, this.finishUpload] }
        this.content = ""
    }

    handle(name) {
        let middlewares = this._middlewareMap[name]
        for (let middleware of middlewares) {
            this.use(middleware)
        }
        this.handleRequest(this)
    }

    // Middlewares

    async beforeUpload (ctx, next) {
        console.log('beforeUpload')
        ctx.content = await ctx.genContent()
        await next()
        console.log('after beforeUpload')
    }

    async startUplaod (ctx, next) {
        console.log('startUplaod')
        let result = await ctx.upload(ctx.content)
        await next()
        console.log('after startUplaod')
    }

    finishUpload (ctx, next) {
        console.log('finishUpload')
        //do something like notify listeners
        console.log('after finishUpload')
    }

    // Helpers
    genContent() {
        return new Promise((resolve, reject) => {
            setTimeout(function () {
                resolve('upload content')
            }, 3)
        })
    }

    upload(content) {
        return new Promise((resolve, reject) => {
            setTimeout(function () {
                resolve(true)
            }, 5)
        })
    }
}

大家可以看到,利用中间件不但可以实现业务隔离,调用也很清晰,只要根据业务调整use的顺序即可。各个中间件还可以随意组合,各组件间也没有依赖关系,自身内聚性高。细心的朋友可以发现,middleware-center 还支持异步方法。得益于中间件的洋葱结构,使得使用者可以处理业务的任何“位置”(如:beforeUpload、startUplaod、finishUpload、after finishUpload、after startUplaod、after beforeUpload)

遍地开花

好思想应该遍地开花,业余时间我用swift简单实现了中间件模型MiddlewareCenter,但还没优化,和处理引用问题,感兴趣的朋友可以一起维护。

class ViewController: UIViewController {
    
    let center = MiddlewareCenter()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        center.use(BeforeUpload())
        center.use(StartUpload())
        center.use(EndUpload())
        
        center.handle(ctx: nil)
    }
}

public class StartUpload: Middleware {
    public override func execute() {
        print("before StartUpload.")
        next?.execute()
        print("after StartUpload.")
    }
}

public class BeforeUpload: Middleware {
    public override func execute() {
        print("before upload.")
        next?.execute()
        print("after upload.")
    }
}

public class EndUpload: Middleware {
    public override func execute() {
        print("before EndUpload.")
        next?.execute()
        print("after EndUpload.")
    }
}

四、总结

中间件实现了业务隔离,满足每个业务所需的数据,又能很好控制业务下发执行的权利,所以“中间件”模式算是一种不错的设计。
理解中间件,主要理解三个概念,包括:context、next、洋葱结构。context为业务所需的上下文,比如koa是处理网络请求的,所以它的context包含request、response;next是业务流的下发控制,使用好next,可以灵活地处理各种业务,包括错误处理、中间件重用等;洋葱结构可以让使用者轻松地处理各个流程的“位置”。

Theory