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 中间件接收
getState和dispatch两个方法组成的对象作为参数; -
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。