1.前言
最近在看redux相关的东西,发现redux也有中间件一说。之前接触的express、koa也有中间件的概念,而axios中也有拦截器这种相似的机制,那就正好梳理下这些概念的原理。阅读本篇文章之前,读者应该对axios、koa、express、redux有所了解。每一部分的原理解析都会结合源码进行,为了方便理解,有些代码顺序做了调整并进行了适当简化。
2.axios拦截器
2.1 注册
const axios = require('axios')
axios.interceptors.request.use((config) => {
console.log('请求拦截器')
// 在发送请求前处理配置,并返回处理后的配置
return config
}, (error) => {
// 发生错误时
return Promise.reject(error)
})
axios.interceptors.response.use((response) => {
console.log('响应拦截器')
// 对响应数据做处理,并返回处理后的数据
return response
}, (error) => {
// 发生错误时
return Promise.reject(error)
})
2.2 原理
axios.interceptors.request/response.use方法接受两个函数类型参数,分别处理正常情况和错误情况。axios 把这两个函数作为 promise.then 的参数,在运行时把所有的拦截器组合成一个promise调用链依次执行
axios发送请求整体的流程是:
- config配置经过请求拦截器处理
- 发送请求
- 获取响应response
- response通过响应拦截器处理
- 将结果返回用户
以 0.21.1 版本为例
// axios/lib/core/Axios.js
var InterceptorManager = require('./InterceptorManager');
//...
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 这里即是请求和响应的拦截器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// axios/lib/core/InterceptorManager.js
function InterceptorManager() {
// 放置拦截器们的数组
this.handlers = [];
}
// ...
// 这里即是注册中间件所用的use方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// ...
// 在拦截器的原型上定义forEach方法,之后会用到
// 这里可以简单理解为提供一个方法遍历this.handlers
InterceptorManager.prototype.forEach = function forEach(fn) {
// ...
};
// axios/lib/core/Axios.js
// ...
// dispatchRequest即为axios真正执行发送请求的方法
var dispatchRequest = require('./dispatchRequest');
// ...
// 定义axios发送请求的方法
Axios.prototype.request = function request(config) {
// ...
// 一开始chain就有两个方法,对应promise.then的两个参数
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 通过unshift把请求拦截器插到数组头部
// 所以请求拦截器实际执行顺序和注册顺序相反
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 把响应拦截器插到数组尾部
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
// chain在插入时总是处理正常流程和错误流程的函数成对的插入,所以这里连续使用shift
// 通过while循环构造promise.then的调用链
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
同上面代码可以看出,所有的拦截器最后都被放在一个长长的promise.then调用链中执行,上一个拦截器处理完的配置/数据会传递给下一个promise.then,这种机制也支持在拦截器中使用async/await
3.koa中间件
3.1 注册
const Koa = require('koa')
const app = new Koa()
async function fn_1(ctx, next) {
console.log('fn_1 start')
await next()
console.log('fn_1 end')
}
app.use(fn_1)
app.listen(3001)
3.2 原理
理解koa的中间件有一个非常经典的洋葱圈模型
koa的中间件最后会组成嵌套的高阶函数,类似于
middlewareA(ctx, () => middlewareB(ctx, () => middlewareC(ctx, ...)))
结合源码看一下,这里使用的koa版本是2.13.1
// koa/lib/application.js
const Emitter = require('events');
const compose = require('koa-compose');
// ...
module.exports = class Application extends Emitter {
constructor(options) {
// ...
// 存放中间件的数组
this.middleware = [];
}
// ...
// 将中间件函数传给use方法
use(fn) {
// ...
this.middleware.push(fn);
return this;
}
callback() {
// 处理中间件
const fn = compose(this.middleware);
// ...
const handleRequest = (req, res) => {
// 创建ctx对象
const ctx = this.createContext(req, res);
// 将compose函数的返回传递给handleRequest
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
// fnMiddleware即是通过compose函数处理的返回结果
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
listen(...args) {
debug('listen');
// 当http服务创建成功后调用回调函数
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
koa在内部通过koa-compose处理,再来看看koa-compose做了什么
// koa-compose/index.js 版本4.1.0
module.exports = compose
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
// 在koa handleRequest中调用这个返回的函数时,next为空
return function (context, next) {
// 记录当前middleware的下标
let index = -1
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 取出中间件
let fn = middleware[i]
// 下面两个if语句保证最后一个中间件调用next不会报错
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(
fn(
context,
// 通过bind方法返回一个函数,实际上就是下一个中间件
// 也就是写中间件时的第二个参数、通常取名为next
// 只要没有取到middleware数组的最后一个元素,dispatch都会递归下去
dispatch.bind(null, i + 1)
)
);
} catch (err) {
return Promise.reject(err)
}
}
// 开始执行第一个中间件
return dispatch(0)
}
}
值得注意的是,koa注册的中间件最后都会被promise.resolve包装一层从而被转换为promsie对象。进而可以使用 await next()
的写法等待下一个中间件执行完毕。
编写中间件时,第二个参数 next 实际上就是下一个中间件,如果已经是最后一个中间件,next执行返回Promise.resolve(),依然可以正常调用。
4.express中间件
4.1 注册
const express = require('express')
const app = express()
async function fn_1(req, res, next) {
console.log('fn_1 start')
next()
console.log('fn_1 end')
}
app.use(fn_1)
app.listen(3002)
4.2 原理
express注册的中间件最后会被处理成一层一层的回调函数。express的源码相对于axios和koa来说感觉更复杂一些。首先,在express中,有一个layer对象用于包装中间件
// express/lib/router/layer.js 版本4.17.1
module.exports = Layer;
function Layer(path, options, fn) {
// ...
// layer的handle方法即为注册的中间件
this.handle = fn;
// ...
}
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle;
// ...
fn(req, res, next);
};
先来看一下express负责路由相关代码中use方法的实现
// express/lib/router/index.js
var Layer = require('./layer');
// ...
var proto = module.exports = function(options) {
// ...
function router(req, res, next) {
// ...
}
// ...
// 存放layer对象的数组
router.stack = [];
return router;
};
// 注册中间件
proto.use = function use(fn) {
// ...
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: false,
end: false
}, fn);
// ...
this.stack.push(layer);
}
// 当有请求命中改路由时实际执行的方法
proto.handle = function handle(req, res, out) {
var self = this;
var idx = 0;
var stack = self.stack;
// ...
function next(err) {
while (idx < stack.length) {
// 取出中间件,并将中间件数组索引加1
var layer = stack[idx++];
// 执行中间件
layer.handle_request(req, res, next)
}
}
next()
}
express中间件的注册可以通过app.use(fn)进行全局注册,也可以通过app.use(path, fn)或者app.method(path, fn)的方式进行局部注册,但最终都会走到router对象的use方法。
从上面的代码可以看出,在编写express中间件时的第三个参数next实际上是通过包装的下一个中间件。由于在包装函数内直接调用下一个中间件,没有针对异步的处理逻辑,且包装函数本身是一个普通的同步函数,自然无法支持用async/await等方式处理处理异步,这就是express中间件不支持异步的根本原因。
5.redux中间件
5.1 注册
import {
createStore,
applyMiddleware,
} from 'redux'
function reducer(state, action) {
let newState = { ...state }
// ...
return newState
}
export function logger({ getState, dispatch }) {
// next 代表下一个中间件包装过后的 dispatch 方法,action 表示当前接收到的动作
return (next) => async (action) => {
console.log('logger before change', action)
// 调用下一个中间件包装的 dispatch
let val = await next(action)
console.log('logger after change', getState(), val)
return val
}
}
export function debug({ getState, dispatch}) {
return (next) => async (action) => {
console.log('debug before change', action)
let val = await next(action)
console.log('debug after change', getState(), val)
return val
}
}
const initialState = {
// ...
}
export default createStore(
reducer,
initialState,
applyMiddleware(logger, debug),
)
5.2 原理
redux中间件的处理逻辑类似于koa的洋葱圈模型,其中包含了各种高阶函数和各种柯里化,有一点不好理解,我们可以先尝试理解这样一种函数,它是一种高阶聚合函数,接受一个函数数组为参数,将后加入数组的函数的执行结果作为参数传递给先加入数组的函数
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
// 简单理解
// compose(fn1, fn2, fn3)(...args) = > fn1(fn2(fn3(...args)))
// 示例
const a = []
a.push(function fn1 (val) { return val })
a.push(function fn2 (val) { return val * 2 })
a.push(function fn3 (val) { return val * 3 })
var x = compose(...a)
x(2) // => a 12
如果能理解compose函数,那接着往下看,我们通过 compose(...a)
生成函数x,即(...a) = > fn1(fn2(fn3(...a)))
,当函数x执行时,fn3、fn2、fn1依次执行,这和三个函数添加到数组中的顺序相反,如果applyMiddleware方法按照参数顺序将中间件填入数组中,那实际执行时越靠后的中间件反而会先执行,这和实际情况不符,而且compose函数也无法处理异步中间件。
这时需要先留意一下redux中间件的编写方式
function logger({ getState, dispatch }) {
return (next) => async (action) => {
console.log('logger before change', action)
// 调用下一个中间件包装的 dispatch
let val = await next(action)
console.log('logger after change', getState(), val)
return val
}
}
中间件函数第一次执行,返回一个函数a;函数A执行,返回一个函数B;函数B的函数体才是中间件真正的代码。好家伙,这和套娃有什么区别…
再来看applyMiddleware的逻辑
// redux/src/compose.js 版本4.0.5
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
// redux/src/applyMiddleware.js 版本4.0.5
export default 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)
}
// 通过这一步数组chain里的元素就是中间件函数中 (next) => async (action) => { ... } 部分
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// todo即形为(...args) => a(b(c(...args)))的聚合函数
// b(c(...args))就是a的next参数,c(...args)就是b的next参数,...args就是c的next参数
const const todo = compose(...chain)
// (...args) => a(b(c(...args)))执行时,函数c/b/a从后往前执行
// c/b/a执行结果就是中间件函数中async (action) => { .. } 部分
// store.diapatch将会成为最后一个中间件的next参数
const dispatch = todo(store.dispatch)
// 如果这里打印dispatch,会发现它就是传递给applyMiddleware的第一个中间件
// 它的next参数就是下一个中间件
return {
...store,
dispatch
}
}
}
5.3 帮助理解
如果上面的解析看不太懂的话,可以尝试运行下面的例子帮助理解
function a() {
return (next) => {
return (action) => {
console.log('a before', action)
const res = next(action)
console.log('a after', res)
}
}
}
function b() {
return (next) => {
return (action) => {
console.log('b before', action)
const res = next(action)
console.log('b after', res)
return res
}
}
}
function c() {
return (next) => {
return (action) => {
console.log('c before', action)
const res = next(action)
console.log('c after', res)
return res
}
}
}
function compose(...funcs) {
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
function applyMiddleware(...funcs) {
const middlewares = new Array(funcs.length)
funcs.forEach((func, index) => {
middlewares[index] = func()
})
return middlewares
}
const chain = applyMiddleware(a, b, c)
console.log(chain)
const todo = compose(...chain)
console.log(todo)
const dispatch = todo((action) => action)
console.log(dispatch)
dispatch({
type: 'SET_LOG',
})
// 输出结果
// a before {type: "SET_LOG"}
// b before {type: "SET_LOG"}
// c before {type: "SET_LOG"}
// c after {type: "SET_LOG"}
// b after {type: "SET_LOG"}
// a after {type: "SET_LOG"}
另外这篇博客也有助于理解redux中间件机制