美文网首页程序员
Middleware(中间件)之道

Middleware(中间件)之道

作者: CatchZeng | 来源:发表于2018-09-26 13:01 被阅读15次

    一、前言

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

    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,可以灵活地处理各种业务,包括错误处理、中间件重用等;洋葱结构可以让使用者轻松地处理各个流程的“位置”。

    相关文章

      网友评论

        本文标题:Middleware(中间件)之道

        本文链接:https://www.haomeiwen.com/subject/brhbgftx.html