对比 Koa 和 Redux中间件解析

315 阅读5分钟

 Koa中间件

原理

koa 把很多 async 函数组成一个处理链,每个 async 函数都可以做一些自己的事情,然后用 await next() 来调用下一个 async 函数。我们把每个 async 函数称为 middleware,这些 middleware 可以组合起来,完成很多有用的功能。koa 的中间件是通过 Async/Await 实现的,中间件执行顺序是“洋葱圈”模型。

原理:中间件之间通过 next 函数联系,当一个中间件调用 next() 后,会将控制权交给下一个中间件,直到下一个中间件不再执行 next() 时沿路返回,依次将控制权交给上一个中间件。 

实现和应用

// 最外层中间件,可以用于兜底 Koa 全局错误
app.use(async (ctx, next) => {
    try {
        // console.log('中间件 1 开始执行')
        // 执行下一个中间件
        await next();
        // console.log('中间件 1 执行结束')
    } catch (error) {
        console.log(`[koa error]: ${error.message}`)
    }
});

// 第二层中间件,可以用于日志记录
app.use(async (ctx, next) => {
    // console.log('中间件 2 开始执行')
    const {
        req
    } = ctx;
    console.log(`req is ${JSON.stringify(req)}`);
    await next();
    console.log(`res is ${JSON.stringify(ctx.res)}`);
    // console.log('中间件 2 执行结束')

});

通过use方法注册和串联中间件,基础代码如下:

use(fn) {
    this.middleware.push(fn);
    return this;
}

中间件被存储进this.middleware数组中,那么中间件是如何被执行的呢?

// 通过 createServer 方法启动一个 Node.js 服务
listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

Koa 框架通过 http 模块的 createServer 方法创建一个 Node.js 服务,并传入 this.callback() 方法

callback() {
    // 从 this.middleware 数组中,组合中间件
    const fn = compose(this.middleware);

    // handleRequest 方法作为 `http` 模块的 `createServer` 方法参数,
   //该方法通过 `createContext` 封装了 `http.createServer` 
    //中的 `request` 和 `response`对象,并将这两个对象放到 ctx 中
    const handleRequest = (req, res) => {
        const ctx = this.createContext(req, res);
        // 将 ctx 和组合后的中间件函数 fn 传递给 this.handleRequest 方法
        return this.handleRequest(ctx, fn);
    };
    return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;

    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // on-finished npm 包提供的方法,该方法在一个 HTTP 请求 closes,
    //finishes 或者 errors 时执行
    onFinished(res, onerror);
    // 将 ctx 对象传递给中间件函数 fnMiddleware
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

执行过程为:

  • 通过compose方法组合各种中间件,返回一个中间件组合函数fnMiddleware

  • 请求过来时,会先调用`handleRequest`方法,该方法完成

  • 调用createContext方法,对该次请求封装出一个ctx对象;

  • 接着调用this.handleRequest(ctx, fnMiddleware)处理该次请求。

  • 通过fnMiddleware(ctx).then(handleResponse).catch(onerror)执行中间件

compose方法组合各种中间件

function compose(middleware) {
    // 这里返回的函数,就是上文中的 fnMiddleware
    return function (context, next) {
        let index = -1
        return dispatch(0)

        function dispatch(i) {
            // 
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            index = i
            // 取出第 i 个中间件为 fn
            let fn = middleware[i]
            if (i === middleware.length) fn = next

            // 已经取到了最后一个中间件,直接返回一个 Promise 实例,进行串联
            // 这一步的意义是保证最后一个中间件调用 next 方法时,也不会报错
            if (!fn) return Promise.resolve()

            try {
                // 把 ctx 和 next 方法传入到中间件 fn 中,并将执行结果使用 Promise.resolve 包装
                // 这里可以发现,我们在一个中间件中调用的 next 方法,其实就是dispatch.bind(null, i + 1),即调用下一个中间件
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}
  • Koa 的中间件机制被社区形象地总结为洋葱模型;

所谓洋葱模型,就是指每一个 Koa 中间件都是一层洋葱圈,它即可以掌管请求进入,也可以掌管响应返回。换句话说:外层的中间件可以影响内层的请求和响应阶段,内层的中间件只能影响外层的响应阶段。

  • dispatch(n)对应第 n 个中间件的执行,第 n 个中间件可以通过await next()来执行下一个中间件,同时在最后一个中间件执行完成后,依然有恢复执行的能力。即,通过洋葱模型,await next()控制调用 “下游”中间件,直到 “下游”没有中间件且堆栈执行完毕,最终流回“上游”中间件。这种方式有个优点,特别是对于日志记录以及错误处理等需要非常友好

Redux 中间件设计和实现

在redux中,中间件的作用在于, 调用 dispatch 触发 reducer之前做一些其他操作,也就是说,它改变的是执行dispatch到 触发 reducer的流程。

Redux中compose

Redux 也实现了一个compose方法,完成中间件的注册和串联

function compose(...funcs: Function[]) {
	return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}

compose方法的执行效果如下代码:

compose([fn1, fn2, fn3])(args)=>
compose(fn1, fn2, fn3) (...args) = > fn1(fn2(fn3(...args)))

简单来说,compose方法是一种高阶聚合,先执行 fn3,并将执行结果作为参数传给 fn2,以此类推。我们使用 Redux 创建一个 store 时,完成对compose方法的调用,Redux 精简源码类比为:

// 这是一个简单的打日志中间件
function logger({
    getState,
    dispatch
}) {
    // next 代表下一个中间件包装过后的 dispatch 方法,action 表示当前接收到的动作
    return next => action => {
        console.log("before change", action);
        // 调用下一个中间件包装的 dispatch 
        let val = next(action);
        console.log("after change", getState(), val);
        return val;
    };
}

// 使用 logger 中间件,创建一个增强的 store
let createStoreWithMiddleware = Redux.applyMiddleware(logger)(Redux.createStore)

function applyMiddleware(...middlewares) {
    // middlewares 为中间件列表,返回一个接受原始 createStore 方法(Redux.createStore)作为参数的函数
    return createStore => (...args) => {
        // 创建原始的 store
        const store = createStore(...args)
        // 每个中间件都会被传入 middlewareAPI 对象,作为中间件参数
        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }

        // 给每个中间件传入 middlewareAPI 参数
        // 中间件的统一模板为 next => action => next(action) 格式
        // chain 中保存的都是 next => action => {next(action)} 的方法
        const chain = middlewares.map(middleware => middleware(middlewareAPI))

        // 传入最原始 store.dispatch 方法,作为 compose 二级参数,compose 方法最终返回一个增强的dispatch 方法
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch // 返回一个增强版的 dispatch
        }
    }
}

Redux 中间件特点总结为:

  • Redux 中间件接收getStatedispatch两个方法组成的对象作为参数;

  • Redux 中间件返回一个函数,该函数接收下一个next方法作为参数,并返回一个接收 action 的新的dispatch方法;

  • Redux 中间件通过手动调用next(action)方法,执行下一个中间件。

对比

也像是一个洋葱圈模型,但是对于同步调用和异步调用稍有不同,以三个中间件为例。

  • 三个中间件均是正常同步调用next(action),则执行顺序为:中间件 1 before next → 中间件 2 before next → 中间件 3 before next → dispatch 方法调用 → 中间件 3 after next → 中间件 2 after next → 中间件 1 after next。

  • 第二个中间件没有调用next(action),则执行顺序为:中间件 1 befoe next → 中间件 2 逻辑 → 中间件 1 after next,注意此时中间件 3 没有被执行

  • 第二个中间件异步调用next(action),其他中间件均是正常同步调用nextt(action),则执行顺序为:中间件 1 before next → 中间件 2 同步代码部分 → 中间件 1 after next → 中间件 2 异步代码部分 before next → 中间件 3 before next → dispatch 方法调用 → 中间件 3 after next → 中间件 2 异步代码部分 after next。