在前端开发中,Redux已经成为了必不可少的技术之一。其状态管理和组件化的模式为我们提供了非常强大的能力处理复杂的业务逻辑。然而,在我们使用该库的时候,我们经常需要引入一些中间件。本文将会介绍 Redux 中间件中的核心概念:Thunk、Saga、Promise等,并详细讲解其用法及注意事项。
Redux Middleware
在 Redux 中,中间件是由一系列的 $store.dispatch()
调用组成,这些调用通过一些定制化的函数来改变 Redux store 的行为。每个中间件都可以访问 store 的 dispatch
和 getState
方法,同时,如果需要赋予 extra arguments(额外的参数)可以复用 compositional currying 技巧来触达每一个经过的中间件函数。同时中间件也会启用其自身的 effects 链,如异步的 actions。中间件允许你去捕捉和解释通过 dispatch
发布的 actions,和当它们到达到 reducer 之前拦截它们。
基本的中间件实现
像这样的一个基础中间件,非常容易写:
const myMiddleware = store => next => action => { // 在这里做一些有意义的事情,可能有些函数会导致异步行为! return next(action); };
这就是一个标准的中间件,在代码中可以添加我们需要的行为,然后我们可以使用 applyMiddleware()
方法调用它们。让我们详细了解在 Redux 中如何使用中间件。
如何使用中间件
Redux 官方库提供了一个名为 applyMiddleware()
的方法,用于将中间件应用到 store 上。applyMiddleware()
接受多个中间件作为参数,而每个中间件是一个函数。
例如,使用 redux-thunk
中间件将看起来像这样:
import { createStore, applyMiddleware } from 'redux'; import thunkMiddleware from 'redux-thunk'; import rootReducer from './reducers'; const store = createStore( rootReducer, applyMiddleware(thunkMiddleware) );
在这个例子中,我们将 thunkMiddleware
作为 applyMiddleware()
的参数。这使得 Redux 的 store 能够理解下面的 action creator 函数:
-- -------------------- ---- ------- -------- --------------------- - ------ ------------------ - ---------------------------------- ------ --------------------------------------------------- ------ -------- -- ---------------- ----- -- --------------- ----- ----------- ------ - ---------- -- -------------------------------- ------ -- -- -
这个函数说了当我们调用 fetchPosts()
时,将发起一个针对 subreddit
的网络 API 请求。dispatch(requestPosts(subreddit))
等价于 store.dispatch(requestPosts(subreddit))
,那么这个 action 将会交由 requestPosts
的 reducer 来处理。在此期间,我们可以渲染一个 UI 以使用户知晓我们正在加载数据。当 Promise 链在网络请求返回值后处理接续时,就会调用 dispatch(receivePosts(subreddit, json))
函数,它等价于:store.dispatch(receivePosts(subreddit, json))
。
至此,我们完成了一个最简单的中间件示例,下面我们会详细分析其中涉及到的概念。
Thunk
Thunk 可以让 action creator 返回一个函数代替一个 action 对象。该函数接收 dispatch
和 getState
方法作为参数,以便在需要的时候以后续的操作。这个额外的函数是在 action 创建函数内部自定义的,因此 action 创建函数自身不需要处理任何副作用。
示例
考虑如下的 action 创建函数:
function incrementAsync() { return dispatch => { setTimeout(() => { dispatch(increment()); }, 1000); }; }
这个函数不再返回一个 action 对象。相反,它在内部设置了 clearTimeout,其目的是延迟一秒钟,并在该时间之后调度一个函数。此函数再次调用 dispatch,此时在 1s 后将使用 increment()
来创建一个 action。
现在,与平常一样,用 connect()
函数把它 connect 到你的组件上。
-- -------------------- ---- ------- ------ - ------- - ---- -------------- ------ - -------------- - ---- ------------------- ----- ------- - -- ------ -------- -- -- - ----- ---------------- ------- ----------- -- ---------------------------- -- --------- ------ -- ----- --------------- - ----- -- -- ------ -------------- --- ------ ------- -------- --------------- -----------
现在,我们可以谈论一下如何使用 thunk。
使用 Thunk
使 store 能够理解使用 thunk 的 action creator 需要用到 redux-thunk
。可以通过引用此包后调用中间件来启用它。
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; const store = createStore( rootReducer, applyMiddleware(thunk) );
此时,无论从 action creator 返回什么类型的值,Redux 都能够接受。如果返回 action 对象,这只是正常工作机制。如果返回一个函数,则 Redux 会在合适时机执行该函数。
Saga
为了更好地管理异步操作,Redux Saga 提供了一种优雅的方式处理副作用。Saga 允许您编写像同步代码一样的异步操作。Redux Saga 具有一组测试确定的行为,使锁定它们变得极其容易。
Redux Saga 是以 Generator 函数作为基础构建的。Generator 函数被称为 Coroutine,它使用简单有助于理解的(短)Pseudoruntimes。“长”运行时间通常意味着,Saga 可以管理连续的异步操作,而无需使用回调或 Promises。另外,由于派生在此框架下的代码易于测试,因此开发者可以为 Saga 内的所有操作编写测试例子。
Saga 在许多方面都与 thunk 相似。它们都可以在 action 创建函数中进行异步操作。然而,在 thunk 中,必须掌握调度(通过 setTimeout 实现并控制回调函数中的代码)。相比之下,Saga 具有具有出色的可处理并发的能力。
Saga 特性
Saga 在设计之初考虑了将几个相关操作同时安排到进程中的复杂功能,是与其他异步中间件(如 thunk)相比,提供了一些显着的优势:
容易测试
与 thunk 中的副作用类似,Saga 中也存在操作副作用。但是,Saga 中的这些副作用都是作为迭代器函数中的简单 yield 语句定义的,并使用相同的技术轻松测试。这使得编写自动化单元测试非常容易。
可取消的操作
Saga 使您能够轻松定义可以并行执行、取消的操作,这是 thunk 所不能及的。这个功能几乎可以包括任何内容,从处理任何形式的异步操作,到有效处理用户交互。Saga 中的任何操作都可以被取消。
Synchronous-looking code
Saga action 是非阻塞协作任务(简单来说,是一种类型的并发任务)生成器。简而言之,它们包含像普通的 JavaScript 代码一样阻塞调用,但这些操作事实上是将异步操作分开执行的一种方法。Saga action 允许你不阻塞 Redux store 的渲染,同时完成长时间的异步操作。此外,Saga 功能使其非常容易管理同步联结操作。
如何使用 Saga
在启动 Saga 之前,库代码需要安装和引用。安装发生在 npm/yarn 中:
npm install --save redux-saga
此时,您已经准备好向 store 应用中间件:
-- -------------------- ---- ------- ------ -------------------- ---- ------------- -- ------ ---- ---------- ----- -------------- - ----------------------- ----- ----- - ------------ ------------ ------------------------------- -- -- --- -- -- ---- ----------- -----------------------------
在这个例子中,我们使用 createSagaMiddleware()
创建了 sagaMiddleware
,然后使用 applyMiddleware()
将其添加到 store 中。
接下来,我们需要为 store 指定一个 rootSaga
。这是我们的集合点,Saga 将从那里开始并发执行,并且可以与其他的 Saga 协同工作,以便针对用户的交互快速内部调用。
我们将来完善这个概念,现在看一个简单的 Saga。
Saga 示例
通过Saga来实现异步控制器,下面是一个切换语言的例子:
-- -------------------- ---- ------- ------ ------------ ---- ------ ---- --------------------- ------ --------- ---------------------- - ----- ----------- ----- ----- ----- ------------------ -------- -------------- --- - ------ --------- --------------------- - ----- ----------------------------- ---------------- -
在这个例子中,我们完全将一个 action 类型 (SWITCH_LANGUAGE
) 的控制器交给了 Saga 来处理。这段代码使用了 takeLatest()
,使得 Saga 在遇到最新的 action 时才实际发送请求。
上面的代码使用了以下几种效果。
put()
:发送一个 actiondelay()
:等待指定毫秒后继续运行 Saga 控制器take(value)
:当 store 中出现指定 value 时返回;不指定时,使用 takeLatest() 监听最新的 action。
上面的 Saga 可以理解为:
- 等待来自 type 为
SWITCH_LANGUAGE
的 action - 在发送的 action 中包含 payload
- 稍等 500ms
- 放置一个 action(此示例中,名为
UPDATE_LANGUAGE
)。
使用 Saga 和类 Promise 逻辑的区别在于 Saga
- 适合处理有序异步操作
- 具有多种各异的假设
- 高度扩展性
- 可测试。
Promise
在上面的两个例子中,都有异步处理。在异步的控制器中,我们需要确保我们设置了异步操作完成之后的 next 步骤。
Promise 对象刚刚好能够做到这点,在异步函数操作进行之后,通过一个箭头函数返回的 Promise 就可以调用下一个函数。这种模式是普遍效用的,因此在一些地方就会发现有实现自动方式的库存在。
例如,我们可以通过 axios 包获取一个 JSON 结果。如下:
import axios from 'axios'; export function getImportData() { return axios.get('/api/import/data').then(res => res.data); }
如此一来,我们将 getImportData()
函数及其返回值纳入了一个 Promise 链,允许我们定义异步控制器及其后的操作。
Promise 实例
考虑国际式纯计算的例子。
-- -------------------- ---- ------- ------ - ------------ - ---- ---------------- ----- ------------- - ------------------------------- ----- ------------- - ------------------------------- ------ ----- ------------ - -- -- --- ----------------- ------- -- ------------- -- --------- --------- --------- ------ ---- --- ----- -- ------ --------- ---------- - --- - ----- ----- - ----- --------------- ----- -------------------------------- - ----- ------- - ----- --------------------- - -
在这个例子中,我们定义了一个 action,使用 createAction()
来生成它的常量。这个函数将一个字符串参数作为 action 类型。我们随后定义 getUserStats()
,一个将返回一个 Promise 的函数。
接下来,我们在一个方案内检查用户的分数以了解其奖励水平:如果用户的分数(从 API 中获取)高于阈值,就向他们发放一个奖品,否则对他们进行些许惩罚。
在这个 Saga 中,我们首先将来自 getUserStats()
的数据解决在 stats
中。如果成功,我们使用 put()
发送了一个设置为该用户得分的 action。否则,我们调用 decreaseScore()
。
在两种情况下,handleError 和 isLoading 的状态都将被更新,同时我们的 Sagas 控制器退出。我们下面还将介绍一些有用的工具,可以帮助更好地管理 Saga。
结论
Redux 中间件是在应用程序的数据流中运行的组件,可以在动作被处理前和处理后抓取和操作它们。了解其中的核心概念能够帮助开发者更好地管理数据流和消除副作用。
Thunk、Saga 和 Promise 是如何轻松地处理异步操作的工具。它们之间有所不同,每种都适合不同种类的需要。Thunk 在简单应用程序中足够使用,而 Saga 适用于开发者要求更高的场景。Promise 可以为开发者提供快速的解决方案,减少了代码重复。在实际开发中,选择合适的工具是一个关键问题。
无论您选择哪种工具,写出复杂、高级别的代码时,每个都有所帮助。请记住,Redux middleware 是可测试的,并且易于管理。在使用时,保持小巧、有组织,并在必要的情况下添加注释,这些都是最佳实践。我希望这篇文章能够帮助您发掘和学习 Redux 中间件的潜力,开发出更优秀的应用。
来源:JavaScript中文网 ,转载请注明来源 https://www.javascriptcn.com/post/66fca26d447136260170ef5c