为什么需要中间件

接触过 Express 的同学对“中间件”这个名词应该并不陌生。在 Express 中,中间件就是一些用于定制对特定请求的处理过程的函数。作为中间件的函数是相互独立的,可以提供诸如记录日志、返回特定响应报头、压缩等操作。

同样的,在 Redux 中,action 对象对应于 Express 中的客户端请求,会被 Store 中的中间件依次处理。如下图所示:

中间件可以实现通用逻辑的重用,通过组合不同中间件可以完成复杂功能。它具有下面特点:

  • 中间件是独立的函数
  • 中间件可以组合使用
  • 中间件有一个统一的接口

这里采用了 AOP (面向切面编程)的思想。

对于面向对象思想,当需要对逻辑增加扩展功能时(如发送请求前的校验、打印日志等),我们只能在所在功能模块添加额外的扩展功能;或者选择共有类通过继承方式调用,但是这将导致共有类的膨胀。否则,就只有将其散落在业务逻辑的各个角落,造成代码耦合。

而使用 AOP 的思想,可以解决代码冗余、耦合问题。我们可以将扩展功能代码单独放入一个切面,待执行的时候才将其载入到需要扩展功能的位置,即切点。这样的好处是不用更改本身的业务逻辑代码,这种通过串联的方式传递调用扩展功能也是中间件的原理。

从零开发一个中间件

中间件需要有统一的接口,才能实现自由组合。每个中间件必须被定义成一个函数 f1,返回一个接收 next 参数的函数 f2,而 f2 又返回一个接收 action 参数的函数 f3next 参数本身也是一个函数,中间件调用 next 函数通知 Redux 处理工作已经结束,可以将 action 对象传递给下一个中间件或 Reducer。

最简单的中间件

例如,可以编写一个什么事都不做的中间件:

function doNothingMiddleware ({ dispatch, getState }) {
return function (next) {
return function (action) {
return next(action)
}
}
}

用箭头函数进行简化:

const doNothingMiddleware = ({ dispatch, getState }) => (next) => (action) => next(action)

可以看出,中间件通过定义函数、接收函数、返回函数,来对 action 对象进行处理。

不管是中间件的实现,还是中间件的使用,都是让每个函数的功能尽量小,然后通过函数的嵌套组合来实现复杂功能,这是函数式编程中的重要思想。

logger

接下来做一些扩展,实现一个可以记录日志的中间件:

const logger = ({ dispatch, getState }) => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', getState())
return result
}

经过这个中间件的处理,每次触发 action 时,会首先打印出当前 action 的信息,然后调用 next(action),将 action 对象传递给下一个中间件或 Reducer,返回处理后的结果,然后获取最新 state 并打印。

crashReporter

一个记录错误日志的中间件:

const crashReporter = ({ dispatch, getState }) => next => action => {
try {
return next(action)
} catch (err) {
console.error('Caught an exception!', err)
throw err
}
}

try ... catch ...next(action) 包裹,对于正常 action 不做任何处理,对于出错的 action 处理将会捕获异常,打印错误日志,并将异常抛出。

在 Redux 中组合使用中间件

在 Redux 中,通过 applyMiddleware 来使用中间件:

import { createStore, combineReducers, applyMiddleware } from 'redux'

const todoApp = combineReducers(reducers)
const store = createStore(
todoApp,
applyMiddleware(logger, crashReporter)
)

通过 store.dispatch(addTodo('use redux')) 触发一个 action,将先后被 loggercrashReporter 两个中间件处理。

接下来我们看看 Redux 是怎么实现中间件调用的,这是 Redux 中的部分源码:

export default function applyMiddleware(
...middlewares: Middleware[]
): StoreEnhancer<any> {
return (createStore: StoreCreator) => <S, A extends AnyAction>(
reducer: Reducer<S, A>,
...args: any[]
) => {
const store = createStore(reducer, ...args)
let dispatch: Dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
} const middlewareAPI: MiddlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return {
...store,
dispatch
}
}
}

假设有三个中间件 M1,M2,M3,应用 applyMiddleware(M1, M2, M3) 将返回一个闭包函数,该函数接收 createStore 函数作为参数,使得创建状态树 store 的步骤在这个闭包内执行;然后将 store 重新组装成 middlewareAPI 作为新的 store,即中间件最外层函数的参数,这样中间件就可以根据状态树进行各种操作了。

对中间件处理的关键逻辑在于

  const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

首先,将 applyMiddleware 函数中传入的中间件按顺序生成一个队列 chain,队列中每个元素都是中间件调用后的结果,它们都具有相同的结构 next => action => {}

然后,通过 compose 方法,将这些中间件队列串联起来。compose 是一个从右向左的嵌套包裹函数,也是函数式编程中的常用范式,实现如下:

export default function compose(...funcs: Function[]) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return <T>(arg: T) => arg
} if (funcs.length === 1) {
return funcs[0]
} return funcs.reduce((a, b) => (...args: any) => a(b(...args)))
}

假设 chain 是包含 C1、C2、C3(对应 M1,M2,M3 第一层函数返回值) 三个函数的数组,那么 compose(...chain)(store.dispatch) 即为 C1(C2(C3(store.dispatch))),而且:

  • applyMiddleware 的最后一个中间件 M3 中的 next 就是原始的 store.dispatch
  • M2 中的 nextC3(store.dispatch)
  • M1 中的 nextC2(C3(store.dispatch))

最终将 C1(C2(C3(store.dispatch))) 作为新的 dispatch 挂载在 store 中返回给用户,作为用户实际调用的 dispatch 方法。由于已经层层调用了 C3,C2,C1,中间件的结构已经从 next => action => {} 被拆解为 acion => {}

我们可以梳理一遍当用户触发一个 action 的完整流程:

  1. 手动触发一个 action:store.dispatch(action)
  2. 等价于调用 C1(C2(C3(store.dispatch)))(action)
  3. 执行 C1 中的代码,直到遇到 next(action),此时的 next 为 M1 中的 next,即:C2(C3(store.dispatch))
  4. 执行 C2(C3(store.dispatch))(action),直到遇到 next(action),此时的 next 为 M2 中的 next,即:C3(store.dispatch)
  5. 执行 C3(store.dispatch)(action),直到遇到 next(action),此时的 next 为 M3 中的 next,即:store.dispatch
  6. 执行 store.dispatch(action)store.dispatch(action) 内部调用 root reducer 更新当前 state
  7. 执行 C3 中 next(action) 之后的代码
  8. 执行 C2 中 next(action) 之后的代码
  9. 执行 C1 中 next(action) 之后的代码

其实这就是所谓的洋葱模型,Koa 中的中间件执行机制也是如此。

对于上面的 applyMiddleware(logger, crashReporter),如果我们执行

export const store = createStore(
counter,
applyMiddleware(logger, crashReporter)
); store.subscribe(() => console.log("store change", store.getState())); store.dispatch({ type: "INCREMENT" });

,结果将是

先触发 logger,输出 dispatching,执行 next(action);然后在 crashReporter 中无异常,没有输出;执行 Reducer,得到新的 state,store 中监听到状态变化,输出 store change;最后执行 logger 中 next 之后的语句

如果是 store.dispatch(),因为 action 必须是一个对象,所以在 crashReporter 中将会捕获异常,并抛出错误,结果为:

demo

redux-thunk

这个应该是最常用到的 Redux 中间件了,是我们在 Redux 中处理异步请求的常用方案。redux-thunk 的实现非常简单,只有14 行代码(包括空行)

function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
} return next(action);
};
} const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware; export default thunk;

其主要逻辑为,检查 action 的类型,如果是函数,就执行 action 函数,并把 dispatchgetState 作为参数传递进去;否则就调用 next 让下一个中间件继续处理 action

所以,我们可以通过使用 redux-thunk 来在 action 生成器(action creator)中返回一个函数而不是简单的 action 对象。从而实现 action 的异步 dispatch,如:

const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
return {
type: INCREMENT_COUNTER,
};
} function incrementAsync() {
return (dispatch) => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}

或在特定条件下才发送 action,如:

function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState(); if (counter % 2 === 0) {
return;
} dispatch(increment());
};
}

参考文章

《深入浅出 React 和 Redux》·程墨

《redux 中间件入门到编写,到改进,到出门》

Redux 中间件与函数式编程的更多相关文章

  1. redux:基于函数式编程的事件处理和状态维护机制

    redux = monand + pipeline + highorder componet + decouple + middleware redex = store based + event h ...

  2. React躬行记(12)——Redux中间件

    Redux的中间件(Middleware)遵循了即插即用的设计思想,出现在Action到达Reducer之前(如图10所示)的位置.中间件是一个固定模式的独立函数,当把多个中间件像管道那样串联在一起时 ...

  3. redux源码解析-函数式编程

    提到redux,会想到函数式编程.什么是函数式编程?是一种很奇妙的函数式的编程方法.你会感觉函数式编程这么简单,但是用起来却很方便很神奇. 在<functional javascript> ...

  4. redux沉思录:基于flux、状态管理、函数式编程的前端状态管理框架

    基于flux和reduce的通信和状态管理机制; 和数据库管理系统一样,redux是一个状态管理系统(或机制). const store = createStore( reducer, compose ...

  5. 【React全家桶入门之十三】Redux中间件与异步action

    在上一篇中我们了解到,更新Redux中状态的流程是这种:action -> reducer -> new state. 文中也讲到.action是一个普通的javascript对象.red ...

  6. Redux:中间件

    redux中间件概念 比较容易理解. 在使用redux时,改变store state的一个固定套路是调用store.dispatch(action)方法,将action送到reducer中. 所谓中间 ...

  7. 转:JavaScript函数式编程(三)

    转:JavaScript函数式编程(三) 作者: Stark伟 这是完结篇了. 在第二篇文章里,我们介绍了 Maybe.Either.IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会 ...

  8. 【大前端攻城狮之路】JavaScript函数式编程

    转眼之间已入五月,自己毕业也马上有三年了.大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户.同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺 ...

  9. 函数式编程-compose与pipe

    函数式编程中有一种模式是通过组合多个函数的功能来实现一个组合函数.一般支持函数式编程的工具库都实现了这种模式,这种模式一般被称作compose与pipe.以函数式著称的Ramda工具库为例. cons ...

随机推荐

  1. opencv python 图像二值化/简单阈值化/大津阈值法

    pip install matplotlib 1简单的阈值化 cv2.threshold第一个参数是源图像,它应该是灰度图像. 第二个参数是用于对像素值进行分类的阈值, 第三个参数是maxVal,它表 ...

  2. 内置json&pickle&shelve&xml

    序列化:把对象(变量)从内存中变成可存储可传输的过程称之为序列化,Python中叫做pickling,其他语言中也被称之为serialization,marshalling,flattening等等 ...

  3. Laplace's equation

    链接:https://en.wikipedia.org/wiki/Laplace%27s_equation

  4. 深度复数网络 Deep Complex Networks

    转自:https://www.jiqizhixin.com/articles/7b1646c4-f9ae-4d5f-aa38-a6e5b42ec475  (如有版权问题,请联系本人) 目前绝大多数深度 ...

  5. DevExpress v19.1新版亮点——WinForms篇(三)

    行业领先的.NET界面控件DevExpress v19.1终于正式发布,本站将以连载的形式介绍各版本新增内容.在本系列文章中将为大家介绍DevExpress WinForms v19.1中新增的一些控 ...

  6. Linux相关TCP参数优化: proc/sys/net/ipv4/ 提高web质量

    tcp_wmem(3个INTEGER变量): min, default, max min:为TCP socket预留用于发送缓冲的内存最小值.每个tcp socket都可以在建议以后都可以使用它.默认 ...

  7. IDEA提交代码到github

    GIT客户端安装及idea配置github账号并提交代码到GIT参考资料:https://blog.csdn.net/qq_31405633/article/details/88193119 1. 选 ...

  8. 最全面的H5的背景音效素材(经过实践),分享给你!!!

    个人内心独白: 这两天在为一个H5的页面寻找一些相关音效,茫茫的网络,辣么大,真是想法设法翻遍你,不说废话了,看总结吧哦 方法总结(这才是重点,看这里): 1.如果是部分铃声截取的,我们可以来到铃声之 ...

  9. CSS中的自适应单位vw、vh、vmin、vmax

    1.vw.vh.vmin.vmax各单位的意义 上面的自适应单位可以统称为视口单位. 可以先了解一下视口指的是什么? 在PC端,视口指的是在PC端,指的是浏览器的可视区域:而在移动端,它涉及3个视口: ...

  10. 【bzoj2073】【[POI2004]PRZ】位运算枚举子集的特技

    (上不了p站我要死了) Description 一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍在桥上的人都不能超过一 ...