写在前面:

​ 在下才疏学浅,此前没有接触过 koa 和 express,对于 redux 也是停留在使用层面,对于源码的理解也都是一己之见,有理解不当和表述错误的地方恳请指正,虚心接受任何建议。写下此文只是为了记录最近对 redux 和 koa 源码的研究和理解,学习的过程中发现这两个框架的代码小巧而精美,阅之赏心悦目,并且发现都有对中间件的支持,遂放在一起比较。

Koa 中间件

基本用法:

1
2
3
4
5
6
7
8
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

koa 的中间件是一个函数签名为 (ctx, next) 的异步函数,ctx 是 koa 封装的 http 上下文对象,next 为进行下一个中间件异步请求的方法(下文会有解释),下面代码为一个简单的 Koa 中间件测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Koa = require("koa");
const { compose } = require("./compose");
const app = new Koa();

const middleWare1 = async (ctx, next) => {
console.log("middleWare1-before");
await next();
console.log("middleWare1-after");
};
const middleWare2 = async (ctx, next) => {
console.log("middleWare2-before");
await next();
console.log("middleWare2-after");
};
const middleWare3 = async (ctx, next) => {
console.log("middleWare3-before");
await next();
console.log("middleWare3-after");
};

app.use(middleWare1);
app.use(middleWare2);
app.use(middleWare3);
app.use(async (ctx, next) => {
console.log("INIT");
});

app.listen(3000);

终端输出结果为:

1
2
3
4
5
6
7
8
$ node koaMiddleWare.js
middleWare1-before
middleWare2-before
middleWare3-before
INIT
middleWare3-after
middleWare2-after
middleWare1-after

发生了什么:

middleWare1 中间件执行后,打印出第一条语句,执行 next 方法时被挂起,进而执行 middleWare2 中间件。middleWare2、middleWare3 中间件重复同样的过程,直到执行最后一个中间件,打印出 INIT,然后被挂起的 middleWare3、middleWare2、middleWare1 分别执行 next 之后的语句。

整个流程:(略去了一些判断)

  • new Koa():创建 koa 实例

  • app.use():将中间件存放在 middleware 数组中,并返回当前实例

    1
    2
    3
    4
    use(fn) {
    this.middleware.push(fn);
    return this;
    }
  • app.listen():

    1
    2
    3
    4
    listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
    }

    调用 nodejs http 模块的 createServer 方法创建 http.Server 实例,并监听传递的端口和其他参数,callback 方法在创建 server 时只被调用一次,其返回值作为 createServer 方法的参数,在每次收到 http 请求时都会被执行,即:传入 ctx 和 fnMiddleware 参数,执行 this.handleRequest 方法,进而执行fnMiddleware(ctx) 方法,对中间件处理的结果 promise 进行 handleResponse 处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    callback() {
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
    };
    return handleRequest;
    }
    handleRequest(ctx, fnMiddleware) {
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
    }

    基本流程弄清了,然后可能对 handleRequest 方法传入的第二个参数 fnMiddleware 感兴趣,此参数是 compose 方法执行后的返回结果,compose 方法是 koa 实现中间件的最关键方法,koa 通过 promise+递归的形式对各类中间件进行处理。分析方法时,请一定牢记方法签名和返回值,先不用考虑方法的内部实现细节。compose 方法的参数为中间件数组,每一个数组项都是一个异步方法,方法返回一个需要 context 和 next 参数的方法,而 this.handleRequest 方法的 fnMiddleware(ctx) 正好传递了其需要的一个参数:上下文对象 ctx,此方法执行后返回 dispatch(0) 的结果,也就是一个 promise 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function compose (middleware) {
    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
    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)
    }
    }
    }
    }
    1
    return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

    fn 函数为传入的各中间件,按数组中的顺序依次执行,并将 fn 执行结果 resolve。编写中间件时,还记不记得第二个参数 next,这里的 next 即为 dispatch.bind(null, i + 1) ,如果 next 方法被执行,则当前中间件被挂起,dispatch 下一个中间件,直到所有中间件都执行完毕 Promise.resolve() 一个空值,整个流程结束。

Redux 中间件

先来看个Redux 中间件的 Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const redux = require("redux");
const { createStore, applyMiddleware } = redux;

const middleWare1 = ({ getState, dispatch }) => next => action => {
console.log("middleWare1-before");
next(action);
console.log("middleWare1-after");
return action;
};
const middleWare2 = ({ getState, dispatch }) => next => action => {
console.log("middleWare2-before");
next(action);
console.log("middleWare2-after");
return action;
};
const middleWare3 = ({ getState, dispatch }) => next => action => {
console.log("middleWare3-before");
next(action);
console.log("middleWare3-after");
return action;
};
const myReducer = (state = {}, action) => {
console.log(action.type);
return state;
};

const enhancer = applyMiddleware(...[middleWare1, middleWare2, middleWare3]);
const store = createStore(myReducer, enhancer);
store.dispatch({ type: "INIT" });

redux 的中间件设计的非常巧妙,核心思想是对 dispatch 方法的一层层包装,你可能会疑惑,redux 的中间件为啥长这样,这些参数又都是啥意思,且听我娓娓道来。koa 的中间件采用 promise+递归的方式实现,await 挂起执行下一个中间件让 koa 容器有天然的优势实现中间件。redux 中间件采用函数式编程思想中的组合+柯里化完成,重点在于对 dispatch 函数的一层层包装和 reduce。第一个参数 { getState, dispatch } 只是为了让中间件具有获取 state 的能力,后两个参数才是中间件的精髓。

redux 有两种方式创建 store,第一种是不传 enhancer 函数(不加入任何中间件),只传递 reducer 和 state 的初始值,这种方式 redux 只是创建基本的 store,另外提一句,redux 的基本组件只包括:state、action、reducer 和 dispatch,如果要扩展的话只能对 dispatch 做文章,redux 的中间件实现其实就是对 dispatch 一层一层的包装。另一种创建 store 的方式为通过 enhancer 方法,如下源码,enhancer 方法接受 createStore 和 reducer、preloadedState 参数,返回创建后的 store

1
2
3
4
5
6
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}

接下来看 enhancer 函数,enhancer 函数是 applyMiddleware 函数执行后返回的东东,果不其然,返回的函数需要 createStore 和一些参数,最后执行后返回一个被中间件改造过的 store 对象,完美契合,但是在创建新 store 时,对 dispatch 方法动了手脚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}

const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

直接来看关键的代码:中间件数组执行了一个 map 操作,返回一个新数组,数组中每项都为 next => action => {//middleware code} 形式的函数,假设上述形式的函数分别为 f、g 和 h,经过 compose 后则变成 F = f(g(h(arg))) ,意为:传入一个参数 arg,h 函数处理后返回结果作为 g 的参数,g 处理后返回 的结果作为 f 的参数,即传入 store.dispatch 后,h 函数返回一个新的 dispatch,并将其作为 g 函数的参数,g 函数返回的 dispatch 作为 f 函数的参数,最后返回一个包装后的 dispatch 函数作为 store 的参数返回,这样每个中间件的 next 都为下一个中间件的 dispatch 方法。至此,我认为 redux 作者对于 next 的命名非常讲究,next 就是下一个中间件的 dispatch 方法。第一次理解时,我疑惑:它不就是个 dispatch 么,为啥叫 next,难道是怕参数名重复,如今再品读,回味无穷!

1
2
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

中间件巧妙运用了函数执行时的不断压栈,无论是 koa 的递归还是 redux 围绕 dispatch 的一层层包装,都围绕着函数进行,以函数为中心,并且对关键数据进行了封装,koa 封装的是HTTP、redux 封装的是 store,学习之路,道阻且长。