尝试理解 Koa、Redux middleware 的演进过程
发布于 5 年前 作者 maquannene 3967 次浏览 来自 分享

之前开发 web 端,使用 Egg 框架,期间实现的一些功能例如:权限检测、操作日志上报等都是基于框架的 middleware 机制件完成的。虽然最后完成了功能,但其实对中间件真正的实现机制、运行时序还不能做到完全的理解。

Egg 是基于 Koa 实现的,Koa 的代码量非常少,加起来也就 1000 多行,涉及到中间件核心的部分,也就不到 100 行,如果有耐心可以直接读 Koa 源码 学习。

我在读完 Koa 中间件代码的后,其实也就只能算是看懂,但是并不知道这个机制是怎么一点一点演变过来的,所以接下来我就试图通过我自己的理解,来讲述一下这个演变的过程。

一、最初阶段

一个最简单的 web 框架,只用实现 request response 之间的部分,即:

  • 接收 request
  • 处理 dispose (这一部分)
  • 返回 response

例如这是一个简单的 dispose:

const dispose = () => {
    console.log('处理请求,然后返回')
}
dispose()

直到渐渐有了一个需求,我们需要在 dispose 前后加一些日志,这时代码演变成了这样:

const dispose = () => {
    console.log('日志 >>>')
    console.log('处理请求,然后返回')
    console.log('日志 <<<')
}
dispose()

这样代码有一个很大的问题,就是非 dispose 的逻辑侵入到了 dispose 的函数中,所以再改一下:

const dispose = () => {
    console.log('处理请求,然后返回')
}

//  这里将 log 放在 dispose 外部,不侵入 dispose 的实现
console.log('日志 >>>')
dispose()
console.log('日志 <<<')

再将前后的 log 进行封装,最终代码演变成了这样:

const dispose = () => {
    console.log('处理请求,然后返回')
}

const log = next => {
    console.log('日志 >>>')
    next()
    console.log('日志 <<<')   
}
const disposeWithLog = log(dispose)
disposeWithLog()

二、中级阶段

除了日志需求,又收到还需要加入打点需求,那么同理实现;

const track = next => {
    console.log('打点 >>>')
    next()
    console.log('打点 <<<')   
}
cosnt disposeWithTrackWithLog = track(disposeWithLog)
disposeWithTrackWithLog()

渐渐的,类似这种需求越来越多,必须要实现一套机制来支持这种需求,我们试图用一个函数来合并所有的类似 log、track 这样的子流程,此时我们管这些子流程叫做中间件。最终达到 合并之后按顺序执行,执行顺序为先进后出,也就是经典的多层嵌套洋葱圈模型:

const combine = (middleware1, middleware2) => next => {
    return middleware1(() => {
        middleware2() => {
            next()
        }
    })
}
const disposeWithTrackWithLog = combine(log, track)(next)

对于 combine 实现,发现 middlewareX(() => {…}) 部分其实上是一样的,所以可以使用递归,来优化 combine 代码,做到对 middlewares 数组的支持,实现如下:

const combine = middlewares => next => {
    const dispatch = mids => {
        const [middleware, ...rest] = mids
        middleware(() => {
            if(rest.length === 0) {
                next()
            }
            else {
                dispatch(rest)
            }
        })
    }
    return () => dispatch(middlewares)
}

在 koa 中,combine 的过程叫做 compose,并且在此基础上,加入上下文 ctx 这个常用的变量,最终的实现如下:

//  中间件
const log = (next, ctx) => {
    console.log('日志 >>>')
    next()
    console.log('日志 <<<')   
}

//  中间件
const track = (next, ctx) => {
    console.log('打点 >>>')
    next()
    console.log('打点 <<<')   
}

//  中间件组合
const compose = middlewares => (next, ctx) => {
    const dispatch = mids => {
        const [middleware, ...rest] = mids
        middleware(() => {
            if(rest.length === 0) {
                next(ctx)
            }
            else {
                dispatch(rest, ctx)
            }
        })
    }
    return () => dispatch(middlewares)
}

const newDispose = compose([log, track])(dispose, 'ctx')
newDispose()

最终,中间件的机制基本就完成了,Koa 的实现大致就是这样,除此之外,Koa 还增加了 promise,但其实原理还是一样的;

三、终极版本

当我还在回味 koa 的实现的时候,又想到 Redux 也有类似的机制,在读了 Redux 的源码之后,感叹其实现的核心代码更加简洁;

仔细看上面的中级阶段代码的实现逻辑,其实就是每次从中间件数组中移出第一个元素用来执行,并且把剩下的中间件当做参数传入继续递归执行。这种剥壳式的调用方式,联想到一个函数,Array.reduce,Redux 就是使用 reduce 实现的终级优雅版本的 compose 函数:

const compose = middlewares => {
    return middlewares.reduce(result, middlreware => (...args) => result(middleware(...args)))
}

这里 Redux 没有把 next 封装到 compose 中,可以借助这个高灵活性,通过小改动,可以轻松加入 ctx 做到和 Koa 一样支持上下文,并且还可以扩展支持 action 参数,Redux 这个中间件实现方式可以说是非常优雅了:

const compose = middlewares => {
    //  这里我个人认为  next, ...rest 这种实现兼容性更强,而不是 Redux 中的 ...args 的实现,不知道作者怎么想的;
    //  我把这个改动提了一个 pr 给 Redux,但是被拒了,个人感觉这个应该是 Redux 的一个 bug,因为 Redux 的实现并不能保证每个中间件接收到的参数都是相同的;
    return middlewares.reduce(result, middlreware => (next, ...rest) => result(middleware(next, ...rest), ...rest))
}

//  中间件
const log = (next, ctx) => action => {
    console.log('日志 >>>')
    next(action)
    console.log('日志 <<<')   
}

//  中间件
const track = (next, ctx) => action => {
    console.log('打点 >>>')
    next(action)
    console.log('打点 <<<')   
}

const newDispose = compose([log, track])(dispose, 'ctx')
newDispose('action')

Redux 也在其官网中,也描述了 middleware 的 演变过程,写的非常流畅易懂,有兴趣可以读一下,了解事物的演进过程,有助于更好的理解事物的本质。

3 回复

Redux 用起来相当繁琐。类型提示也不够友好。

思路没啥问题的,加油

回到顶部