聊聊大前端领域 Middleware 的几种实现方式?

共 7560字,需浏览 16分钟

 ·

2021-05-29 15:30

点击上方 程序员成长指北,关注公众号

回复1,加入高级 Node 进阶交流群

一、前言

Middleware(中间件)本意是指位于服务器的操作系统之上,管理计算资源和网络通信的一种通用独立的系统软件服务程序。分布式应用软件借助这种软件在不同的技术之间共享资源。而在大前端领域,Middleware 的含义则简单得多,一般指提供通用独立功能的数据处理函数。典型的 Middleware 包括日志记录、数据叠加和错误处理等。本文将横向对比大前端领域内各大框架的 Middleware 使用场景和实现原理,包括Express, Koa, ReduxAxios

二、大前端领域的Middleware

这里说的大前端领域自然就包括了服务器端和客户端了。最早提出 Middleware 概念的是Express, 随后由原班人马打造的Koa不但沿用了 Middleware 的架构设计,还更加彻底的把自己定义为中间件框架

Expressive HTTP middleware framework for node.js

在客户端领域,Redux也引入了 Middleware 的概念,方便独立功能的函数对 Action 进行处理。Axios虽然没有中间件,但其拦截器的用法却跟中间件十分相似,也顺便拉进来一起比较。下面的表格横向比较了几个框架的中间件或类中间件的使用方式。

框架 use注册 next调度 compose编排 处理对象
Express Y Y N req & res
Koa Y Y Y ctx
Redux N Y Y action
Axios Y N N config/data

下面我们一起来拆解这些框架的内部实现方式。

三、Express

3.1 用法

app.use(function logMethod(req, res, next) {
console.log('Request Type:', req.method)
next()
})

Express的 Middleware 有多种层级的注册方式,在此以应用层级的中间件为例子。这里看到 2 个关键字,usenextExpress通过use注册,next触发下一中间件执行的方式,奠定了中间件架构的标准用法。

3.2 原理

原理部分会对源码做极端的精简,只保留核心。

Middleware 注册(use)
var stack = [];
function use(fn) {
stack.push(fn);
}
Middleware 调度(next)
function handle(req, res) {
var idx = 0;
next();
function next() {
var fn = stack[idx++];
fn(req, res, next)
}
}

当请求到达的时候,会触发handle方法。接着next函数从队列中顺序取出 Middleware 并执行。

四、Koa

4.1 用法

app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

Express相比,Koa的 Middleware 注册跟路由无关,所有的请求都会经过注册的中间件。同时Koa与生俱来支持async/await异步编程模式,代码风格更加简洁。至于洋葱模型什么的大家都清楚,就不废话了。

4.2 原理

Middleware 注册(use)
var middleware = [];
function use(fn) {
middleware.push(fn);
}
Middleware 编排(koa-compose)
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
// middleware执行完的后续操作,结合koa的源码,这里的next=undefined
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)
}
}
}
}

Express类似,Koa的 Middleware 也是顺序执行的,通过dispatch函数来控制。代码的编写模式也很像:调用dispatch/next -> 定义dispatch/next -> dispatch/next作为回调递归调用。这里有个地方要注意下,对于 Middleware 来说,它们的await next()实际上就是await dispatch(i)。当执行到最后一个 Middleware 的时候,会触发条件if (i === middleware.length) fn = next,这里的nextundefined,会触发条if (!fn) return Promise.resolve(),继续执行最后一个 Middleware await next()后面的代码,也是洋葱模型由内往外执行的时间点。

五、Redux

Redux是我所知的第一个将 Middleware 概念应用到客户端的前端框架,它的源码处处体现出函数式编程的思想,让人眼前一亮。

5.1 用法

const logger = store => next => action => {
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
const crashReporter = store => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
}
}
const store = createStore(appReducer, applyMiddleware(logger, crashReporter))

Redux中间件的参数做过柯里化,storeapplyMiddleware内部传进来的,nextcompose后传进来的,actiondispatch传进来的。这里的设计确实十分巧妙,下面我们结合源码来进行分析。

5.2 原理

Middleware 编排(applyMiddleware)
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
// 先执行一遍middleware,把第一个参数store传进去
chain = middlewares.map(middleware => middleware(middlewareAPI))
// 传入原始的dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}

这里compose的返回值又重新赋值给dispatch,说明我们在应用内调用的dispatch并不是store自带的,而是经过 Middleware 处理的升级版。

Middleware 编排(compose)
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)))
}

compose的核心代码只有一行,像套娃一样的将 Middleware 一层一层的套起来,最底层的args就是store.dispatch

六、Axios

Axios中没有 Middleware 的概念,但却有类似功能的拦截器(interceptors),本质上都是在数据处理链路的 2 点之间,提供独立的、配置化的、可叠加的额外功能。

6.1 用法

// 请求拦截器
axios.interceptors.request.use(function (config) {
config.headers.token = 'added by interceptor';
return config;
});
// 响应拦截器
axios.interceptors.response.use(function (data) {
data.data = data.data + ' - modified by interceptor';
return data;
});

Axios的 interceptors 分请求和响应 2 种,注册后会自动按注册的顺序执行,无需像其他框架一样要手动调用next()

6.2 原理

interceptors 注册(use)
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};

可以看到Axios内部会维护 2 个 interceptors,它们有独立的 handlers 数组。use就是往数组添加元素而已,跟其它框架不同的是这里的数组元素不是一个函数,而是一个对象,包含fulfilledrejected 2 个属性。第二个参数不传的时候rejected就是 undefined。

任务编排
// 精简后的代码
Axios.prototype.request = function request(config) {
config = mergeConfig(this.defaults, config);
// 成对的添加元素
var requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

var chain = [dispatchRequest, undefined];

Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);

promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}

这里通过 promise 的链式调用,将 interceptors 串联了起来,执行顺序是:requestInterceptorChain -> chain -> responseInterceptorChain。这里有一个默认的约定,chain 里的元素都是按照[fulfilled1, rejected1, fulfilled2, rejected2]这种模式排列的,所以注册 interceptors 的时候如果没有提供第二个参数,也会有一个默认值 undefined。

七、各框架的横向对比

看了各大框架的 Middleware 实现方式之后,我们可以总结出以下几个特点:

  • Middleware 机制既可以用于服务器端也可以用于客户端
  • Middleware 机制本质上是向框架使用者开放数据处理链路上的一个或多个点,增强框架的数据处理能力
  • 绝大多数的 Middleware 都是不依赖于具体业务的可复用的功能
  • 多个 Middleware 可以组合起来实现复杂功能

我们再来总结一下各大框架中间件系统实现方式的精髓:

框架 实现方式
Express 递归调用next
Koa 递归调用dispatch
Redux Array.reduce实现函数嵌套
Axios promise.then链式调用

这里面最精妙也是最难理解的就是Array.reduce这种形式,需要反复的推敲。promise.then链式调用的任务编排方法也十分巧妙,前面处理完的数据会自动传给下一个then。递归调用的形式则最好理解,KoaExpress实现的基础上天然支持异步调用,更符合服务器端场景。

八、总结

本文从使用方式入手,结合源码讲解了各大前端框架中 Middleware 的实现方式,横向对比了他们之间的异同。当中的递归调用、函数嵌套和 promise 链式调用的技巧非常值得我们借鉴学习。

最后

如果觉得这篇文章还不错
点击下面卡片关注我
来个【分享、点赞、在看】三连支持一下吧

   “分享、点赞在看” 支持一波  

浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报