axios拦截器/koa中间件/express中间件/redux中间件的原理

24 篇文章 0 订阅
4 篇文章 0 订阅

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发送请求整体的流程是:

  1. config配置经过请求拦截器处理
  2. 发送请求
  3. 获取响应response
  4. response通过响应拦截器处理
  5. 将结果返回用户

以 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中间件机制

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值