中间件
写在前面:
在下才疏学浅,此前没有接触过 koa 和 express,对于 redux 也是停留在使用层面,对于源码的理解也都是一己之见,有理解不当和表述错误的地方恳请指正,虚心接受任何建议。写下此文只是为了记录最近对 redux 和 koa 源码的研究和理解,学习的过程中发现这两个框架的代码小巧而精美,阅之赏心悦目,并且发现都有对中间件的支持,遂放在一起比较。
Koa 中间件
基本用法:
1 | const Koa = require('koa'); |
koa 的中间件是一个函数签名为 (ctx, next) 的异步函数,ctx 是 koa 封装的 http 上下文对象,next 为进行下一个中间件异步请求的方法(下文会有解释),下面代码为一个简单的 Koa 中间件测试:
1 | const Koa = require("koa"); |
终端输出结果为:
1 | node koaMiddleWare.js |
发生了什么:
middleWare1 中间件执行后,打印出第一条语句,执行 next 方法时被挂起,进而执行 middleWare2 中间件。middleWare2、middleWare3 中间件重复同样的过程,直到执行最后一个中间件,打印出
INIT
,然后被挂起的 middleWare3、middleWare2、middleWare1 分别执行 next 之后的语句。
整个流程:(略去了一些判断)
new Koa():创建 koa 实例
app.use():将中间件存放在 middleware 数组中,并返回当前实例
1
2
3
4use(fn) {
this.middleware.push(fn);
return this;
}app.listen():
1
2
3
4listen(...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
13callback() {
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
18function 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 | const redux = require("redux"); |
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 | if (typeof enhancer !== 'undefined') { |
接下来看 enhancer 函数,enhancer 函数是 applyMiddleware 函数执行后返回的东东,果不其然,返回的函数需要 createStore 和一些参数,最后执行后返回一个被中间件改造过的 store 对象,完美契合,但是在创建新 store 时,对 dispatch 方法动了手脚:
1 | function applyMiddleware(...middlewares) { |
直接来看关键的代码:中间件数组执行了一个 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 | const chain = middlewares.map(middleware => middleware(middlewareAPI)) |
中间件巧妙运用了函数执行时的不断压栈,无论是 koa 的递归还是 redux 围绕 dispatch 的一层层包装,都围绕着函数进行,以函数为中心,并且对关键数据进行了封装,koa 封装的是HTTP、redux 封装的是 store,学习之路,道阻且长。