为什么我们需要中间件来实现Redux中的异步流程?
根据文档,“没有中间件,Redux商店只支持同步数据流”。 我不明白为什么会这样。 为什么容器组件不能调用异步API,然后dispatch
这些操作?
例如,想象一个简单的用户界面:一个字段和一个按钮。 当用户按下按钮时,该字段将填充来自远程服务器的数据。
import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';
const ActionTypes = {
STARTED_UPDATING: 'STARTED_UPDATING',
UPDATED: 'UPDATED'
};
class AsyncApi {
static getFieldValue() {
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve(Math.floor(Math.random() * 100));
}, 1000);
});
return promise;
}
}
class App extends React.Component {
render() {
return (
<div>
<input value={this.props.field}/>
<button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
{this.props.isWaiting && <div>Waiting...</div>}
</div>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
field: React.PropTypes.any,
isWaiting: React.PropTypes.bool
};
const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
switch (action.type) {
case ActionTypes.STARTED_UPDATING:
return { ...state, isWaiting: true };
case ActionTypes.UPDATED:
return { ...state, isWaiting: false, field: action.payload };
default:
return state;
}
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
(state) => {
return { ...state };
},
(dispatch) => {
return {
update: () => {
dispatch({
type: ActionTypes.STARTED_UPDATING
});
AsyncApi.getFieldValue()
.then(result => dispatch({
type: ActionTypes.UPDATED,
payload: result
}));
}
};
})(App);
export default class extends React.Component {
render() {
return <Provider store={store}><ConnectedApp/></Provider>;
}
}
当渲染导出的组件时,我可以单击该按钮并正确更新输入。
请注意connect
呼叫中的update
功能。 它分派一个动作,告诉App它正在更新,然后执行异步调用。 调用完成后,提供的值将作为另一个操作的有效负载分派。
这种方法有什么问题? 为什么我要使用Redux Thunk或Redux Promise,如文档所示?
编辑:我搜索Redux回购的线索,并发现行动创作者被要求在过去的纯功能。 例如,这里是一个试图为异步数据流提供更好解释的用户:
动作创建器本身仍然是一个纯函数,但它返回的thunk函数不需要,它可以执行异步调用
动作创作者不再需要纯粹。 因此,过去肯定需要thunk / promise中间件,但似乎不再是这种情况了?
这种方法有什么问题? 为什么我要使用Redux Thunk或Redux Promise,如文档所示?
这种方法没有错。 这在大型应用程序中很不方便,因为您将有不同的组件执行相同的操作,您可能需要去除某些操作,或者保留一些本地状态,如自动递增ID接近动作创建者等。将操作创建者分解为独立功能的维护角度。
您可以阅读我对“如何使用超时发送Redux操作”的答案以获得更详细的演练。
像Redux Thunk或Redux Promise这样的中间件只是给你“语法糖”来调度thunk或promises,但是你不必使用它。
所以,没有任何中间件,你的动作创造者可能看起来像
// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}
但是对于Thunk Middleware,你可以这样写:
// action creator
function loadData(userId) {
return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
);
}
// component
componentWillMount() {
this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}
所以没有太大的区别。 我喜欢后一种方法的一件事是组件不关心动作创建者是异步的。 它只是调用dispatch
正常,它也可以使用mapDispatchToProps
来绑定这样的动作创建者与一个简短的语法等。组件不知道如何实现动作创建者,并且你可以切换不同的异步方法(Redux Thunk,Redux Promise, Redux Saga)而不更改组件。 另一方面,对于前者,显式方法,组件准确知道特定的调用是异步的,并且需要通过某种约定来传递dispatch
(例如,作为同步参数)。
也想想这个代码将如何改变。 假设我们想要第二个数据加载功能,并将它们合并到一个动作创建器中。
采用第一种方法时,我们需要注意我们正在呼叫什么样的动作创建者:
// action creators
function loadSomeData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(dispatch, userId) {
return fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(dispatch, userId) {
return Promise.all(
loadSomeData(dispatch, userId), // pass dispatch first: it's async
loadOtherData(dispatch, userId) // pass dispatch first: it's async
);
}
// component
componentWillMount() {
loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}
使用Redux Thunk动作创建者可以dispatch
其他动作创建者的结果,甚至不用考虑它们是同步还是异步:
// action creators
function loadSomeData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
function loadOtherData(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
);
}
function loadAllData(userId) {
return dispatch => Promise.all(
dispatch(loadSomeData(userId)), // just dispatch normally!
dispatch(loadOtherData(userId)) // just dispatch normally!
);
}
// component
componentWillMount() {
this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}
通过这种方法,如果您稍后希望动作创建者查看当前的Redux状态,则可以使用传递给Thunk的第二个getState
参数,而不必修改调用代码:
function loadSomeData(userId) {
// Thanks to Redux Thunk I can use getState() here without changing callers
return (dispatch, getState) => {
if (getState().data[userId].isLoaded) {
return Promise.resolve();
}
fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
);
}
}
如果您需要将其更改为同步,则也可以在不更改任何调用代码的情况下执行此操作:
// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
return {
type: 'LOAD_SOME_DATA_SUCCESS',
data: localStorage.getItem('my-data')
}
}
因此,使用中间件如Redux Thunk或Redux Promise的好处是,组件不知道动作创建者如何实现,他们是否在意Redux状态,它们是同步还是异步,以及他们是否调用其他动作创建者。 缺点是有些间接,但我们相信它在实际应用中是值得的。
最后,Redux Thunk和朋友只是Redux应用程序中异步请求的一种可能方法。 另一个有趣的方法是Redux Saga,它可以让您定义长时间运行的守护进程(“sagas”),它们会在进行操作时执行操作,并在输出操作之前转换或执行请求。 这将动作创作者的逻辑转化为传奇。 你可能想要检查一下,然后选择最适合你的东西。
我搜索Redux回购中的线索,发现Action Creator在过去被要求是纯粹的功能。
这是不正确的。 文档说,但文档是错误的。
行动创造者从未被要求成为纯粹的功能。
我们修复了文档以反映这一点。
你没有。
但是...你应该使用redux-saga :)
丹·阿布拉莫夫的答案是正确的约redux-thunk
但我会讲一点关于终极版,传奇那是相当类似,但功能更强大。
必要的VS声明
redux-thunk
势在必行/ redux-saga
是声明性的 当你手中有一个thunk时,就像IO monad或承诺一样,一旦执行,你就不会轻易知道它会做什么。 测试一个thunk的唯一方法是执行它,并模拟调度器(或整个外部世界,如果它与更多的东西交互...)。
如果你正在使用mock,那么你不会进行函数式编程。
通过副作用的镜头来看,嘲笑是一个标志,你的代码是不纯的,在功能程序员的眼中,证明某些东西是错误的。 我们不应该下载一个图书馆来帮助我们检查冰山是否完好无损,而应该围绕它进行。 一位硬核TDD / Java人曾经问我你在Clojure中如何做嘲弄。 答案是,我们通常不会。 我们通常将其视为需要重构代码的标志。
资源
这些传奇(因为他们在redux-saga
)是声明性的,就像Free monad或React组件一样,它们在没有任何模拟的情况下测试也更容易。
另请参阅这篇文章:
在现代FP中,我们不应该编写程序 - 我们应该编写程序的描述,然后我们可以随意反思,转换和解释。
(事实上,Redux-saga就像一个混合体:流动势在必行,但效果是陈述性的)
混乱:行动/事件/命令...
前端世界中存在很多关于如CQRS / EventSourcing和Flux / Redux等后端概念如何相关的困惑,主要是因为在Flux中我们使用术语“动作”,它有时可能代表命令式代码( LOAD_USER
)和事件( USER_LOADED
)。 我相信,像事件采购一样,你只应该派遣事件。
在实践中使用传奇
设想一个带有用户个人资料链接的应用程序。 用这两种中间件处理这种情况的惯用方法是:
redux-thunk
<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>
function loadUserProfile(userId) {
return dispatch => fetch(`http://data.com/${userId}`)
.then(res => res.json())
.then(
data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
);
}
redux-saga
<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>
function* loadUserProfileOnNameClick() {
yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}
function* fetchUser(action) {
try {
const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
yield put({ type: 'USER_PROFILE_LOADED', userProfile })
}
catch(err) {
yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
}
}
这个传奇转化为:
每次用户名被点击时,获取用户配置文件,然后用加载的配置文件发送一个事件。
正如你所看到的, redux-saga
有一些优点。
takeLatest
的使用允许表达您只想获取最后一次用户名单击数据的兴趣(处理并发问题,以防用户点击很多用户名时速度很快)。 这种东西很难与thunk。 如果你不想要这种行为,你可以使用takeEvery
。
你保持行动的创造者纯洁。 请注意,保留actionCreators(在sagas put
和组件dispatch
)仍然有用,因为它可能会帮助您在将来添加操作验证(断言/流/打字稿)。
由于效果是声明式的,因此您的代码变得更加可测试
您不需要再触发类似于actions.loadUser()
rpc调用。 你的用户界面只需要发送什么就已经发生了。 我们只会发起事件 (总是过去式),而不是行为了。 这意味着您可以创建解耦的“鸭子”或有界上下文,并且该传奇可以充当这些模块化组件之间的耦合点。
这意味着你的观点更容易管理,因为他们不再需要包含已经发生的事情和应该发生的事情之间的翻译层
例如,想象一个无限的滚动视图。 CONTAINER_SCROLLED
可能会导致NEXT_PAGE_LOADED
,但它是否真的是可滚动容器的责任,以决定我们是否应该加载另一个页面? 然后他必须意识到更复杂的东西,比如最后一页是否成功加载,或者是否已经有试图加载的页面,或者没有更多的项目需要加载? 我不这么认为:为了获得最大的可重用性,可滚动容器应该只是描述它已被滚动。 加载页面是该滚动的“商业效应”
有人可能会争辩说,发电机本身可以用本地变量隐藏redux存储以外的状态,但是如果你开始通过启动定时器等来编排thunk中的复杂事物,那么无论如何你都会遇到相同的问题。 还有一个select
效果,现在允许从你的Redux商店获得一些状态。
Sagas可以实现时间旅行,还可以实现复杂的流量记录和当前正在开发的开发工具。 以下是已经实现的一些简单的异步流日志记录:
解耦
萨加斯不仅取代了雷克斯。 他们来自后端/分布式系统/事件采购。
这是一个非常常见的误解,传奇只是在这里取代你的redux thunk更好的可测试性。 实际上,这只是一个关于redux-saga的实现细节。 使用声明性效果比用于可测试性的thunk要好,但saga模式可以在命令式或声明式代码之上实现。
首先,传奇是一个软件,它允许协调长时间运行的事务(最终一致性)和跨不同有界上下文(域驱动的设计术语)的事务。
为了简化这个前端世界,想象一下widget1和widget2。 当点击widget1上的某个按钮时,它应该对widget2有效。 而不是将两个小部件连接在一起(即,小部件1派发一个针对小部件2的动作),小部件1仅调度其按钮被点击。 然后,这个传奇听这个按钮点击,然后通过分散widget2意识到的新事件来更新widget2。
这增加了简单应用程序不必要的间接级别,但可以更容易地扩展复杂的应用程序。 现在,您可以将widget1和widget2发布到不同的npm存储库,以便他们永远不必知道其他每个人,也不必让他们共享全局注册表操作。 这两个小部件现在是可以分开生活的有界环境。 他们不需要彼此保持一致,并且可以在其他应用程序中重用。 这个传奇是两个小部件之间的一个耦合点,它们以一种有意义的方式为您的业务进行协调。
关于如何构建Redux应用程序的一些不错的文章,您可以在其中使用Redux-saga来解耦原因:
一个具体的用例:通知系统
我希望我的组件能够触发应用内通知的显示。 但我不希望我的组件与具有自己业务规则的通知系统高度耦合(同时显示最多3个通知,通知排队,4秒显示时间等)。
我不希望我的JSX组件决定何时显示/隐藏通知。 我只是让它能够请求通知,并将复杂的规则留在传奇中。 这种东西很难通过thunk或promises来实现。
我在这里描述了如何用传奇完成这件事
为什么叫做佐贺?
术语传奇来自后端世界。 我最初在一次长时间的讨论中向Yassine(Redux-saga的作者)介绍了这个术语。
最初,该术语是在一篇论文中引入的,传奇模式应该被用来处理分布式事务中的最终一致性,但其使用已被后端开发人员扩展到更广泛的定义,因此它现在也涵盖了“流程管理器”模式(不知何故原始传奇模式是流程管理器的一种特殊形式)。
今天,“传奇”一词很混乱,因为它可以描述两种不同的东西。 由于它在Reduce-saga中使用,它没有描述处理分布式事务的方式,而是一种协调应用程序中的操作的方式。 redux-saga
也可能被称为redux-process-manager
。
也可以看看:
备择方案
如果您不喜欢使用生成器的想法,但您对繁体模式及其解耦属性感兴趣,那么也可以使用redux-observable来实现,它使用名称epic
来描述完全相同的模式,但使用RxJS。 如果你已经熟悉Rx,你会感到宾至如归。
const loadUserProfileOnNameClickEpic = action$ =>
action$.ofType('USER_NAME_CLICKED')
.switchMap(action =>
Observable.ajax(`http://data.com/${action.payload.userId}`)
.map(userProfile => ({
type: 'USER_PROFILE_LOADED',
userProfile
}))
.catch(err => Observable.of({
type: 'USER_PROFILE_LOAD_FAILED',
err
}))
);
一些减少传奇有用的资源
2017建议
yield put(someActionThunk)
发送thunk。 如果您害怕使用Redux-saga(或Redux-observable),但只需要解耦模式,请检查redux-dispatch-subscribe:它允许侦听调度并在侦听器中触发新的调度。
const unsubscribe = store.addDispatchListener(action => {
if (action.type === 'ping') {
store.dispatch({ type: 'pong' });
}
});
简短的回答 :对我来说,这似乎是一个完全合理的解决异步问题的方法。 有几个警告。
在开展一项我们刚开始工作的新项目时,我有一条非常类似的思路。 我是香草Redux优雅系统的忠实粉丝,用于更新商店和重组组件的方式,使其不受React组件树的影响。 对于我来说,看起来很奇怪,因为我会陷入这种优雅的dispatch
机制来处理异步。
我最终采用了一种非常类似的方法来处理我们在我们的项目中考虑的库中存在的内容,我们称之为react-redux-controller。
由于以下原因,我最终不会采用上述确切方法:
dispatch
自己。 这限制了一旦connect
语句失控时重构的选项 - 而且仅仅使用这种update
方法看起来相当笨拙。 所以你需要一些系统让你编写这些调度器功能,如果你把它们分解成单独的模块。 综合起来,您必须搭建一些系统以允许dispatch
和商店注入调度功能以及事件的参数。 我知道这种依赖注入的三种合理方法:
dispatch
中间件方法一起工作,但我认为它们基本相同。 connect
函数,而不是直接与原始的标准化商店一起工作。 this
背景下,通过各种可能的机制。 更新
对我来说,这个难题的一部分是反应减少的局限。 connect
的第一个参数获得状态快照,但不派遣。 第二个参数获取调度,但不是状态。 这两个参数都不会得到一个在当前状态下关闭的thunk,因为在继续/回调时能够看到更新的状态。