Middleware(中间件)之道

一、前言

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,567评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,472评论 2 59
  • 1.类 a.理论上的定义 b.编程思想 c.类的声明格式 d.构造方法,init方法 e.对象属性和对象方法 f....
    nothingpy阅读 276评论 0 1
  • 可能一开始就错了吧,想想也觉得够残忍,其实谁都不是谁的谁,也没有一开始就合适那m一说,早是真的爱,不可能不服输,可...
    娜挞阅读 388评论 0 0
  • 今天早上发生了这样的一件事:我每天早上坐在班车上,都会觉得这50分钟的时间非常美好,我可以吃东西、看书、听书或是再...
    叮当叮当当阅读 152评论 0 0