在本章中,我们将学习如何在 React 本机应用程序中发送和接收数据。首先,我们将使我们的应用程序更加动态,更加依赖于后端服务器。您将了解 Thunk 模式,它非常适合 Flux。然后,我们将深入研究更高级的库 redux saga,它基于一种效果模式。这两种解决方案都将使我们的应用程序能够与服务器无缝地交换数据。我还将向您介绍一些更高级的通信模式,例如HATEOAS
和GraphQL
。尽管这两种模式对于 React 本机开发人员来说很少是至关重要的,但是如果有一天,这些模式在 React 本机世界中也变得流行,您会发现理解起来会容易得多。
在本章中,您将学习如何执行以下操作:
- 创建一个假 API
- 从后端获取数据并将其存储在应用程序中
- 设计动作创建者并从容器中分离获取逻辑
- 使用 Redux Thunk 有条件地分派操作
- 编写自己的迭代器和生成器
- 从严重依赖发电机的传奇故事中获益
为了在不依赖外部源的情况下测试各种 API,我们将创建自己的本地 API。您不需要知道任何后端语言,也不需要知道如何公开 API。在本章中,我们将使用一个特殊的库,该库基于我们提供的 JSON 文件构建 API。
到目前为止,我们已经制作了一个整洁的应用程序来显示任务。现在,让我们使用自己的 RESTAPI,而不是加载本地数据文件。克隆要启动的任务应用程序。(我将使用第 5 章、存储模式、*、*目录中示例二的代码)
Representational State Transfer (REST) is a set of rules that put constraints on web services. One of the crucial requirements is statelessness, which guarantees the server will not store the client's data, but instead rely only on the request data. This should be sufficient enough to send a reply to the client.
为了创建假 API,我们将使用json-server
库。库需要一个 JSON 文件;大多数例子称之为db.json
。该库基于该文件创建一个静态 API,用于根据请求发送数据。
让我们首先使用以下命令安装库:
yarn global add json-server
If you prefer to avoid global
, please remember to provide a relative path to the node_modules/json-server/bin
in the following scripts.
库的 JSON 文件应如下所示:
{
"tasks": [
// task objects separated by comma
]
}
幸运的是,我们的tasks.json
文件符合这一要求。我们现在可以启动本地服务器了。打开package.json
并添加一个名为server
的新脚本,如下所示:
// src / Chapter 6 / Example 1 / package.jsonn
// ...
"scripts": {
// ...
"server": "json-server --watch ./src/data/tasks.json"
},
// ...
您现在可以键入yarn run server
来启动它。数据将在http://localhost:3000/tasks
处公开。只需使用浏览器访问 URL 即可检查其是否有效。正确设置的服务器应打印以下数据:
[
{
"name": "Task 1",
"description": "Task 1 description",
"likes": 239
},
// ... other task objects
]
现在我们可以进一步学习如何使用端点。
首先,让我们从一些基本的东西开始。React Native 实现了 Fetch API,它现在是进行 REST API 调用的标准。
目前,我们有一个默认任务列表正在从taskReducer.js
中的文件加载。老实说,从文件或 API 加载都很耗时。最初最好将任务列表设置为清空数组,并使用微调器或文本消息向用户提供反馈,通知他们正在加载数据。我们将在对 fetchapi 进行更改的同时实现这一点。
首先,从 reducer 中的文件中删除数据导入。更改以下声明:
(state = Immutable.List([...data.tasks]), action) => {
// ...
}
并将其替换为此代码段中的代码:
(state = Immutable.List([]), action) => {
// ...
}
Loading data from a file is also a side effect and should undergo similarly restrictive patterns to data fetching. Don't be fooled by the previous implementation that we used to synchronously load data. This shortcut was taken only to concentrate on the specific learning material.
启动应用程序以查看空列表。现在我们添加一个加载指示器,如下所示:
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native';
// ...
const TaskList = ({ tasks, isLoading }) => (
<View>
{isLoading
? <ActivityIndicator size="large" color="#0000ff" />
: tasks.map((task, index) => (
// ...
))
}
</View>
);
在某些情况下,加载需要很长时间,您将需要处理更复杂的场景:数据正在加载,但用户仍可能同时添加任务。在以前的实现中,只有从服务器检索到数据后,才会显示任务。解决这一问题的简单方法是,无论isLoading
道具是什么,如果我们有任务,总是显示任务,这意味着需要一些其他数据:
// src / Chapter 6 / Example 2 / src / views / TaskList.js
const TaskList = ({ tasks, isLoading }) => (
<View>
{isLoading && <ActivityIndicator size="large" color="#0000ff" />}
{tasks.map((task, index) => (
// ...
))}
</View>
);
由于我们有一个基于isLoading
属性显示的加载指示器,我们需要考虑我们的获取过程可能产生的其他状态。
在大多数用例中,Fetch 需要三种状态:
- 启动:取数启动,应使
isLoading
为true
- 成功:数据取数成功
- 错误:Fetch 无法检索数据;应显示相应的错误消息
我们需要处理的最后一个状态是错误。关于用户体验指南,有几种方法:
- 在列表中显示错误消息这为关心表中数据的人提供了一条清晰的消息。它可能包括可单击的链接或重试按钮。您可以将此方法与下面的方法混合使用。
- 显示关于故障的浮动通知这将在其中一个角落显示有关错误的消息。消息可能在几秒钟后消失
- 显示错误模式这会停止用户通知他们错误;它可能包含重试和取消等操作。
我想在这里采取的方法是第一种。实现起来相当容易,我们需要添加一个error
道具,并在此基础上显示一条消息:
const TaskList = ({
tasks, isLoading, hasError, errorMsg
}) => (
<View>
{hasError &&
<View><Text>{errorMsg}</Text></View>}
{hasError && isLoading &&
<View><Text>Fetching again...</Text></View>}
{isLoading && <ActivityIndicator size="large" color="#0000ff" />}
{tasks.map((task, index) => (
// ...
))}
</View>
);
// ...
TaskList.defaultProps = {
errorMsg: 'Error has occurred while fetching tasks.'
};
现在,让我们获取一些数据并使标记完全可用。首先,我们将遵循 React 初学者的方法:在一个有状态组件中使用fetch
。在我们的情况下,它将是App.js
:
// src / Chapter 6 / Example 2 / src / App.js
class TasksFetchWrapper extends React.Component {
constructor(props) {
super(props);
// Default state of the component
this.state = {
isLoading: true,
hasError: false,
errorMsg: '',
tasks: props.tasks
};
}
componentDidMount() {
// Start fetch and on completion set state to either data or
// error
return fetch('http://localhost2:3000/tasks')
.then(response => response.json())
.then((responseJSON) => {
this.setState({
isLoading: false,
tasks: Immutable.List(responseJSON)
});
})
.catch((error) => {
this.setState({
isLoading: false,
hasError: true,
errorMsg: error.message
});
});
}
render = () => (
<AppView
tasks={this.state.tasks}
isLoading={this.state.isLoading}
hasError={this.state.hasError}
errorMsg={this.state.errorMsg}
/>
);
}
// State from redux passed to wrapper.
const mapStateToProps = state => ({ tasks: state.tasks });
const AppContainer = connect(mapStateToProps)(TasksFetchWrapper);
这种方法有许多缺点。首先,它不遵循 fetchapi 文档。让我们读一读这段至关重要的话:
"The Promise returned from fetch won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, it will resolve normally (with ok status set to false), and it will only reject on network failure or if anything prevented the request from completing."
- Fetch API documentation, available at: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch.
如您所见,前面的实现缺少 HTTP 错误处理。
第二个问题是状态复制,我们维护一个 Redux 状态,然后将任务复制到本地组件状态,甚至使用已获取的内容覆盖它。通过连接两个数组,我们可以更加关注任务中已经存在的内容,并找到避免再次存储任务的方法。
此外,如果 Redux 状态发生变化,那么之前的组件将完全忽略更新。这太糟糕了,让我们想办法解决这个问题。
在本节中,我们将学习Thunk 模式以及如何将其与Redux Thunk库一起使用。首先,我们需要使用 Redux 将我们的幼稚和错误的实现从上一节重构为一个。
与其依赖组件状态,不如将其提升到 Redux 存储。请注意我们这里使用的Immutable.Map
。此外,ADD_TASK
操作现在正在使用Immutable.js
中的update
功能:
// src / Chapter 6 / Example 3 / src / reducers / taskReducer.js
const taskReducer = (state = Immutable.Map({
entities: Immutable.List([]),
isLoading: false,
hasError: false,
errorMsg: ''
}), action) => {
switch (action.type) {
case TasksActionTypes.ADD_TASK:
if (!action.task.name) {
return state;
}
return state.update('entities', entities => entities.push({
name: action.task.name,
description: action.task.description,
likes: 0
}));
default:
return state;
}
};
由于我们已经更改了减速器,因此需要修复有状态组件。它应该通过操作委托给 Redux 存储,而不是拥有自己的状态。但是,我们将在稍后实施这些行动:
// src / Chapter 6 / Example 3 / src / App.js
class TasksFetchWrapper extends React.Component {
componentDidMount() {
TaskActions.fetchStart();
return fetch('http://localhost:3000/tasks')
.then(response => response.json())
.then((responseJSON) => {
TaskActions.fetchComplete(Immutable.List(responseJSON));
})
.catch((error) => TaskActions.fetchError(error));
}
render = () => <AppView tasks={this.props.tasks} />;
}
It is wise to move fetching logic to a separate service. This will enable other components to share the same function once they need to trigger fetch too. This is your homework.
您可以将操作分派给构造函数,而不是componentDidMount
。然而,这可能会产生对功能组件进行重构的诱惑。这将是一场灾难,因为每次重新渲染时都会开始抓取。此外,componentDidMount
对我们来说更安全,因为如果在操作上下文中出现任何可能减慢应用程序速度的计算,我们 100%确信用户已经可以看到ActivityIndicator
现在,转到操作实现。你应该能够自己写。如有问题,请参见src / Chapter 6 / Example 3 / src / data / TaskActions.js
。现在我们将重点讨论扩展减速器。这是相当多的工作,因为我们需要处理所有三种动作类型:FETCH_START
、FETCH_COMPLETE
和FETCH_ERROR
,如下所示:
// src / Chapter 6 / Example 3 / src / reducers / taskReducer.js
const taskReducer = (state = Immutable.Map({
// ...
}), action) => {
switch (action.type) {
case TasksActionTypes.ADD_TASK: {
// ...
}
case TasksActionTypes.TASK_FETCH_START: {
return state.update('isLoading', () => true);
}
case TasksActionTypes.TASK_FETCH_COMPLETE: {
const noLoading = state.update('isLoading', () => false);
return noLoading.update('entities', entities => (
// For every task we update the state
// Homework: do this in bulk
action.tasks.reduce((acc, task) => acc.push({
name: task.name,
description: task.description,
likes: 0
}), entities)
));
}
case TasksActionTypes.TASK_FETCH_ERROR: {
const noLoading = state.update('isLoading', () => false);
const errorState = noLoading.update('hasError', () => true);
return errorState.update('errorMsg', () => action.error.message);
}
default: {
return state;
}
}
};
基本上就是这样。最后,您还需要更新视图以使用新结构Immutable.Map
,如下所示:
// src / Chapter 6 / Example 3 / src / views / AppView.js
// ...
<TaskList
tasks={props.tasks.get('entities')}
isLoading={props.tasks.get('isLoading')}
hasError={props.tasks.get('hasError')}
errorMsg={props.tasks.get('errorMsg')}
/>
// ...
对这段代码有一些改进。我现在不谈它们,因为它们是高级主题,涉及到更一般的 JavaScript 函数编程概念。您将在第 8 章、JavaScript 和 ECMAScript 模式中了解镜头和选择器。
要看到前面重构的好处可能很难。这些重构中的一些只有在你做了几天后才会发光。例如,需要重新获取给定事件上的任务。此事件发生在应用程序的完全不同部分,并且未连接到任务列表。在 naive 实现中,您需要处理更新过程并使所有内容保持最新。您还需要向另一个组件公开一个fetch
函数。这将使这两者紧密结合。灾难相反,正如您所看到的,您可能更愿意将抓取逻辑复制到第二个分离的组件。同样,最终会出现代码重复。因此,您将创建由这两个组件共享的父服务。不幸的是,抓取与状态紧密耦合,因此您也会将状态移动到服务。然后,您将进行一些黑客攻击,例如使用 closure 在服务中存储数据。正如您所看到的,这是解决这些问题的一个顺利方法。
但是,在使用 Redux 存储时,只有一个集中式状态只能通过还原器更新。抓取是使用精心设计的操作将数据发送到还原器。获取可以在一个单独的服务中执行,该服务由需要获取任务的组件共享。现在我们将介绍一个库,它可以使所有这些东西变得更干净。
在经典的 Redux 中,如果没有中间件,就无法分派非纯对象的对象。使用 Redux Thunk,您可以通过调度函数延迟调度:
"Redux Thunk middleware allows you to write action creators that return a function instead of an action. The thunk can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met. The inner function receives the store methods dispatch and getState as parameters."
- Redux Thunk official documentation, available at: https://github.com/reduxjs/redux-thunk.
例如,您可以分派一个函数。这样的函数有两个参数:dispatch
和getState
。此函数尚未到达 Redux 还原程序。它只会延迟老式的 Redux 调度,直到执行必要的检查,例如,基于当前状态的检查。一旦我们准备好分派,我们将使用作为function
参数提供的dispatch
函数:
function incrementIfOdd() {
return (dispatch, getState) => {
const { counter } = getState();
if (counter % 2 === 0) {
return;
}
dispatch(increment());
};
}
dispatch(incrementIfOdd())
在上一节中,我指出,fetch
调用可以是一个单独的函数。如果您还没有完成家庭作业,下面是一个重构示例:
const fetchTasks = () => {
TaskActions.fetchStart();
return fetch('http://localhost:3000/tasks')
.then(response => response.json())
.then((responseJSON) => {
TaskActions.fetchComplete(Immutable.List(responseJSON));
})
.catch(error => TaskActions.fetchError(error));
};
class TasksFetchWrapper extends React.Component {
componentDidMount = () => this.props.fetchTasks();
render = () => <AppView tasks={this.props.tasks} />;
}
const mapStateToProps = state => ({ tasks: state.tasks });
const mapDispatchToProps = dispatch => ({ fetchTasks });
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(TasksFetchWrapper);
然而,我们所谓的ActionCreators
与dispatch
紧密耦合,因此不仅创造动作,而且创造dispatch
。让我们通过解除派遣来减轻他们的责任:
// Before
const Actions = {
addTask(task) {
AppDispatcher.dispatch({
type: TasksActionTypes.ADD_TASK,
task
});
},
fetchStart() {
AppDispatcher.dispatch({
type: TasksActionTypes.TASK_FETCH_START
});
},
// ...
};
// After
const ActionCreators = {
addTask: task => ({
type: TasksActionTypes.ADD_TASK,
task
}),
fetchStart: () => ({
type: TasksActionTypes.TASK_FETCH_START
}),
// ...
};
现在,我们需要确保将前面的行动发送到相关的地方。这可以通过传递到dispatch
来实现,如下所示:
const ActionTriggers = {
addTask: dispatch => task => dispatch(ActionCreators.addTask(task)),
fetchStart: dispatch => () => dispatch(ActionCreators.fetchStart()),
fetchComplete: dispatch =>
tasks => dispatch(ActionCreators.fetchComplete(tasks)),
fetchError: dispatch =>
error => dispatch(ActionCreators.fetchError(error))
};
For those experienced in programming, this step may look a little like we are repeating ourselves. We are duplicating function parameters and the only thing we gain is the invocation of dispatch. We can fix this with functional patterns. Such improvements will be made as part of Chapter 8, JavaScript and ECMAScript Patterns. Additionally, please note that in this book, I'm not writing many tests. Once you make writing tests a habit, you will quickly appreciate such easily testable code.
完成此操作后,我们现在可以调整容器组件,如图所示:
// src / Chapter 6 / Example 4 / src / App.js
export const fetchTasks = (dispatch) => {
TaskActions.fetchStart(dispatch)();
return fetch('http://localhost:3000/tasks')
.then(response => response.json())
.then(responseJSON =>
TaskActions.fetchComplete(dispatch)(Immutable.List(responseJSON)))
.catch(TaskActions.fetchError(dispatch));
};
// ...
const mapDispatchToProps = dispatch => ({
fetchTasks: () => fetchTasks(dispatch),
addTask: TaskActions.addTask(dispatch)
});
好的,这是一个伟大的重构,但 Redux Thunk 在哪里?这是一个很好的问题。我确实有意延长了这个例子。在许多 React 和 React 原生项目中,我看到过对 Redux Thunk 和其他库的过度使用。我不想让你成为另一个不理解 Redux Thunk 的目的并滥用它所赋予的权力的开发者。
Redux Thunk 主要允许您决定有条件地分派。通过 Thunk 功能访问dispatch
并不是什么特别的事情。主要的好处是第二个论点getState
。这使您可以访问当前状态并根据其中的值进行决策。
这些强大的工具可能会导致你创造不纯的还原剂。怎样您可以创建一个setter reducer,而不是创建一个真正的 reducer,其工作方式与类中的 set 函数类似。这样的减缩器只会被调用来设置值;但是,该值将在 Thunk 函数中使用getState
函数进行计算。这是完全反模式的,可能导致严重违反比赛条件。
现在我们知道了危险,让我们继续讨论 Thunks 的真正用法。想象一种情况,你想有条件地做出决定。你将如何进入州政府发表声明?一旦我们在 Redux 中使用connect()
函数,这就变得复杂了。我们传递给connect
的mapDispatchToProps
函数无法访问状态。但是我们需要它,所以这里有一个 Redux-Thunk 的有效用法。
The following is good to know: how would we make an escape hatch if we could not use Redux Thunk? We could pass part of the state to the render
function, and then invoke the original function with the expected state. The if
statement could be done with a regular if
in JSX. This could, however, lead to serious performance issues.
是时候在我们的案例中使用 Redux Thunk 了。您可能已经注意到,我们的数据集不包含 ID。如果我们两次获取任务,这将是一个巨大的问题,因为我们没有机制来判断哪些任务已经添加,哪些任务已经存在于我们的 UI 中。当前添加所有已获取任务的方法将导致任务重复。对于我们损坏的体系结构,第一个预防机制是在isLoading
为true
时停止提取。
A real-life scenario would either use IDs or refresh all the tasks on fetch. If so, ADD_TASK
would need to guarantee changes in the backend server.
In the era of Progressive Web Apps, we need to stress this problem even further. Take the case where a connection is lost before adding a new task. If your UI adds the task locally and schedules a backend update, once the network connection is resolved you may run into a race condition: this means that tasks are being refreshed before your ADD_TASK
update is propagated in the backend system. As a result, you would end up with a task list that will not contain the added task until you refetch all tasks from the backend. This may be extremely misleading and should not happen in any financial institution.
让我们实现这个简单的预防机制来说明 Redux Thunk 的功能。首先,使用以下命令安装库:
yarn add redux-thunk
然后,我们需要将thunk
中间件应用到 Redux 中,如下所示:
// src / Chapter 6 / Example 4 / src / data / AppStore.js
import { combineReducers, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
// ...
const store = createStore(rootReducer, applyMiddleware(thunk));
从现在起,我们可以分派函数。现在我们修复fetch
函数以避免多个请求:
// src / Chapter 6 / Example 5 / src / App.js
export const fetchTasks = (dispatch, getState) => {
if (!getState().tasks.isLoading) {
// ...
}
return null;
};
// ...
const mapDispatchToProps = dispatch => ({
fetchTasks: () => dispatch(fetchTasks),
// ...
});
如您所见,这是一个非常简单的用例。请明智地使用 Redux Thunk,不要滥用它赋予您的权力。
Thunk 是另一种不特定于 React 或 Redux 的模式。实际上,它在许多硬核解决方案中使用得相当广泛,例如编译器。
Thunk 是一种延迟评估直到无法避免的模式。解释这一点的初学者示例之一是简单加法。此处显示了一个示例:
// immediate calculation, x equals 3
let x = 1 + 2;
// delayed calculation until function call, x is a thunk
let x = () => 1 + 2;
一些更复杂的用法,例如在函数式语言中,可能在整个语言中都依赖于此模式。因此,仅当终端应用层需要计算时才执行计算。通常,不会执行提前计算,因为此类优化是开发人员的责任。
到目前为止,我们可以使用fetch
执行简单的 API 调用,并且我们知道如何组织代码以便重用。然而,在某些领域,如果我们的应用程序需要,我们可以做得更好。在深入研究 Redux 传奇之前,我想介绍两种新模式:迭代器和生成器。
"Processing each of the items in a collection is a very common operation. JavaScript provides a number of ways of iterating over a collection, from simple for loops to map and filter. Iterators and Generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behavior of for...of loops."
- JavaScript guide on MDN web docs at: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators.
顾名思义,迭代器允许您对集合进行迭代。为此,集合需要实现一个 iterable 接口。在 JavaScript 中,没有接口,因此迭代器只实现一个函数。
"An object is an iterator when it knows how to access items from a collection one at a time, while keeping track of its current position within that sequence. In JavaScript an iterator is an object that provides a next method which returns the next item in the sequence. This method returns an object with two properties: done and value."
- JavaScript guide on MDN web docs https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
以下是 MDN web 文档中此类函数的示例:
function createArrayIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
生成器类似于迭代器;然而,在这里,您可以在函数中迭代仔细设计的断点。生成器返回一个迭代器。返回的迭代器迭代上述断点,每次都从函数返回一些值。
为了表示函数是生成器,我们使用一个特殊的*****
符号,例如function* idGenerator()
。请在下面的代码段中找到一个示例生成器函数。生成器使用yield
关键字返回当前迭代步长值。如果调用了迭代器的next()
函数,迭代器将在下一行恢复,如下所示:
function* numberGenerator(numMax) {
for (let i = 0; i < numMax; i += 1) {
yield console.log(i);
}
}
const threeNumsIterator = numberGenerator(3);
// logs 0
threeNumsIterator.next();
// logs 1
threeNumsIterator.next();
// logs 2
threeNumsIterator.next();
// logs nothing, the returned object contains a key 'done' set to true
threeNumsIterator.next();
首先,我们创建一个generator
函数。Generator
函数需要一个参数。根据提供的参数,生成器知道何时停止生成新数字。在函数之后,我们创建一个示例数字迭代器并迭代其值。
Redux 传奇在很大程度上依赖于生成器模式。多亏了这种方法,我们可以将副作用完全解耦成传奇故事,就像它们是一条独立的线索一样。从长远来看,它使用起来很方便,并提供了一些功能上的优势。其中一些依赖于可组合性,sagas 易于测试,并提供更干净的流来执行异步代码。所有这些现在听起来都不清楚,所以让我们深入了解一下。
This book does not touch much on React, Redux, and React Native testing. This topic would lengthen this book significantly and, I believe, deserves a separate book. However, I will stress how important it is to test your code. This information box is to remind you about testing in Redux Sagas. In different places on the internet (GitHub, forums, Stack Overflow) I have seen this mentioned over and over again: sagas are much easier to test than Thunks. Check this on your own—you will not regret it.
首先,完成安装库和应用中间件的初级步骤。这些步骤可以在官方的 Redux Saga 自述文件中找到,该文件位于https://redux-saga.js.org/ 。
现在是创建第一个传奇并将其添加到我们的rootSaga.
中的时候了。您还记得抓取任务的情况吗?可以从许多地方请求它们(许多解耦的小部件或特性)。saga 的方法与我们以前的解决方案类似,因此让我们看看如何在以下示例中实现它:
// src / Chapter 6 / Example 6 / src / sagas / fetchTasks.js
function* fetchTasks() {
const tasks = yield call(ApiFetch, 'tasks');
if (tasks.error) {
yield put(ActionCreators.fetchError(tasks.error));
} else {
const json = yield call([tasks.response, 'json']);
yield put(ActionCreators.fetchComplete(Immutable.List(json)));
}
}
// whereas ApiFetch is our own util function
// you will want to make a separate file for it
// and take care of environmental variables to determine right endpoint
const ApiFetch = path => fetch(`http://localhost:3000/${path}`)
.then(response => ({ response }))
.catch(error => ({ error }));
我们的fetchTasks
故事非常简单:首先,它获取任务,然后检查是否发生了错误,或者发送错误事件,或者发送成功事件,并附上获取的数据。
我们如何触发fetchTasks
传奇?为了说服你为什么传奇故事如此强大,让我们更进一步。假设我们的代码库是解耦的,一些特性几乎同时请求任务。如何防止触发多个获取任务作业?Redux Saga 库为此提供了现成的解决方案:throttle
函数。
"throttle(ms, pattern, saga, ...args) Spawns a saga on an action dispatched to the Store that matches pattern. After spawning a task it's still accepting incoming actions into the underlaying buffer, keeping at most 1 (the most recent one), but in the same time holding up with spawning new task for ms milliseconds (hence its name - throttle). Purpose of this is to ignore incoming actions for a given period of time while processing a task."
- Official Redux Saga documentation: https://redux-saga.js.org/docs/api/.
我们的用例将非常简单:
// src / Chapter 6 / Example 6 / src / sagas / fetchTasks.js
function* watchLastFetchTasks() {
yield throttle(2000, TasksActionTypes.TASK_FETCH_START, fetchTasks);
}
fetchTasks
功能将在TASK_FETCH_START
事件上执行。在两秒钟内,同一事件不会导致另一个fetchTasks
函数执行。
就这样。最后几件事之一是在rootSaga
中加入前面的故事。这不是一个非常有趣的部分,但是,如果您感到好奇,我建议您查看代码库中的完整示例,可在上找到 https://github.com/Ajdija/hands-on-design-patterns-with-react-native 。
在具有定义良好的例程的更复杂的应用程序中,Redux 传奇胜过 Redux Thunk。一旦您遇到需要取消、重新运行或回复部分流的情况,如何使用 Thunks 或普通 Redux 来完成这些工作还不清楚。有了可组合的 saga 和维护良好的迭代器,您可以轻松地完成它。甚至官方文件也提供了解决这些问题的方法。(参考本章末尾的进一步阅读部分。)
如此强大的库的阴暗面在于它在棕地应用程序中的使用存在问题。这类应用程序的功能可能是以基于承诺或 Thunk 的方式编写的,可能需要进行重大的重构,才能像 greenfield 应用程序一样轻松地与 sagas 一起使用。例如,从 Thunk 函数调用 saga 并不是那么容易,也不能像在 saga 中那样等待已调度函数。连接两个世界可能有好的方法,但这真的值得吗?
在本章中,我们重点讨论了网络模式及其带来的副作用。我们通过简单的模式,然后使用市场上可用的工具。您已经了解了 Thunk 模式,以及迭代器和生成器模式。这三种模式在您未来的编程生涯中都会很有用,无论您是否使用 React Native。
至于 React 生态系统,您已经学习了 Redux Thunk 和 Redux Saga 库的基础知识。它们都解决了大规模应用程序带来的一些挑战。明智地使用它们,并记住我在本章中提出的所有警告。
现在我们知道了如何显示数据、设置数据样式和获取数据,我们准备学习一些应用程序构建模式。也就是说,在下一章中,您将学习导航模式。在 React Native 中,有很多解决这些问题的方法,我非常乐意教您如何选择符合项目需要的方法。
- 编写测试 Redux 官方文档: https://redux.js.org/recipes/writing-tests 。
- 执行撤销历史记录 Redux 官方文档: https://redux.js.org/recipes/implementing-undo-history 。
- 服务器呈现 Redux 官方文档: https://redux.js.org/recipes/server-rendering 。
- 规范化州 Redux 官方文件: https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape 。 这在网络模式中非常重要。从后端系统获取的一些数据需要规范化。
- 异步操作 Redux 官方文档: https://redux.js.org/advanced/async-actions 。
- Redux Saga 配方 Redux Saga 官方文件: https://redux-saga.js.org/docs/recipes/ 。 此资源特别有价值,因为它提供了使用 sagas 进行节流、去抖动和撤销的方法。
- Redux Saga 频道–Redux Saga 官方文档:
"Until now we've used the take and put effects to communicate with the Redux Store. Channels generalize those Effects to communicate with external event sources or between Sagas themselves. They can also be used to queue specific actions from the Store."
- Redux Saga Official Documentation: https://redux-saga.js.org/docs/advanced/Channels.html.
- 关于 Thunks、sagas、抽象和可重用性的惯用 redux 思想: https://blog.isquaredsoftware.com/2017/01/idiomatic-redux-thoughts-on-thunks-sagas-abstraction-and-reusability/ 。
- 资源库:React-Redux 链接/Redux 副作用: https://github.com/markerikson/react-redux-links/blob/master/redux-side-effects.md 。
- 关于传奇的传奇:
"The term saga is commonly used in discussions of CQRS to refer to a piece of code that coordinates and routes messages between bounded contexts and aggregates. However, [...] we prefer to use the term process manager to refer to this type of code artifact." A Saga on Sagas - Microsoft Docs: https://docs.microsoft.com/en-us/previous-versions/msp-n-p/jj591569(v=pandp.10).
- GraphQL 是治疗副作用的另一种方法。GraphQL 是 API 的查询语言,包括前端和后端。在此处了解更多信息: https://graphql.org/learn/ 。
- Redux 可观察-一个恶作剧和传奇的竞争对手。引入反应式编程模式: https://github.com/redux-observable/redux-observable 。 还请查看 RxJS,一个 JavaScript 的反应式编程库: https://github.com/reactivex/rxjs 。
- 表征状态转移: https://en.wikipedia.org/wiki/Representational_state_transfer 。
- HATEOAS(REST 架构的一个组件): https://en.wikipedia.org/wiki/HATEOAS 。