发布于 2026-01-06 0 阅读
0

Redux 中的异步操作:使用 RxJS 和 Redux Observable 简介 获取数据

使用 RxJS 和 Redux Observable 在 Redux 中实现异步操作

介绍

正在获取数据

介绍

Redux是什么?

Redux是一个非常棒的库。如果你还不了解 Redux,它是一个用于 JavaScript 应用的可预测状态容器。简单来说,它充当了应用状态的单一数据源。这个状态,或者Redux store ,只能通过分发action来修改。这些 action 由reducer处理,reducer 会根据分发的 action 类型来决定状态的修改方式。如果你还不熟悉 Redux,可以点击这个链接了解更多信息。

现在,Redux 最常与 React 结合使用,但它并不局限于 React——它可以与任何其他视图库一起使用。

Redux 的问题

然而,Redux 有一个非常严重的问题——它本身对异步操作的处理并不理想。一方面,这确实不好;但另一方面,Redux 只是一个库,它的作用是为你的应用程序提供状态管理,就像 React 只是一个视图库一样。它们都不是完整的框架,你需要根据不同的需求选择合适的工具。有些人认为这是件坏事,因为没有统一的解决方案;而有些人,包括我,则认为这是好事,因为你不会被任何特定的技术所束缚。这很好,因为每个人都可以选择最适合自己需求的技术。

处理异步操作

现在,有一些库提供了用于处理异步操作的 Redux 中间件。我刚开始使用 React 和 Redux 时,我参与的项目使用的是Redux-Thunk。Redux -Thunk 允许你编写返回函数而不是普通对象的 action creator(默认情况下,Redux 中的所有 action 都必须是普通对象),这反过来又允许你延迟分发某些 action。

当时我刚开始学习 React/Redux,Thunk 简直太棒了。它们易于编写和理解,而且不需要任何额外的函数——你基本上就是在用不同的方式编写 action 创建器。

然而,一旦你开始使用 React 和 Redux 进行开发,你就会发现,尽管 thunk 非常易用,但它们并非完美无缺,原因有三:1. 你很容易陷入回调地狱,尤其是在进行 API 请求时;2. 你需要在回调函数或 reducer 中塞满处理数据的业务逻辑(说实话,你不可能每次都获得格式完美的数据,尤其是在使用第三方 API 时);3. 它们难以测试(你需要使用 spy 方法来检查 dispatch 是否传递了正确的对象)。因此,我开始寻找其他更合适的解决方案。就在那时,我发现了Redux-Saga

Redux Saga 非常接近我想要的功能。根据其官网的描述,Saga 的核心理念是将 Saga 看作是应用程序中一个独立的线程,专门负责处理副作用。简单来说,Saga独立于主应用程序运行,监听已分发的 action——一旦 Saga 监听的 action 被分发,它就会执行一些产生副作用的代码,例如 API 调用。它还允许你在 Saga 内部分发其他 action,并且易于测试,因为 Saga 返回的是普通的Effects对象。听起来很棒,对吧?

Redux-Saga 的确存在一些不足之处,而且对大多数开发者来说,这些不足之处还相当突出——它使用了 JavaScript 的生成器函数,而生成器函数的学习曲线相当陡峭。当然,我必须赞扬 Redux Saga 的开发者们使用了 JS 的这项强大功能(你懂我的意思吧,嘿嘿),但是,我个人感觉生成器函数用起来很不自然,至少对我来说是这样。即使我知道它们的工作原理和使用方法,我就是无法真正上手使用它们。这就好比你听广播里播放的某个乐队或歌手的歌,你并不反感,但你绝对不会想到自己去播放。正因如此,我才继续寻找能够处理异步操作的 Redux 中间件。

Redux-Saga 还有一点处理得不太好,那就是取消已经分发的异步操作——例如 API 调用(Redux Observable 由于其响应式特性,在这方面做得很好)。

下一步

大约一周前,我翻看我和朋友大学时写的一个旧 Android 项目,发现里面有一些 RxJava 代码,于是心想:如果 Redux 有响应式中间件会怎么样?所以我做了一些研究,结果,老天爷听到了我的祈祷:Redux Observable出现了

那么Redux Observable什么呢?它是 Redux 的另一个中间件,允许你以函数式、响应式和声明式的方式处理异步数据流。这意味着什么呢?这意味着你可以编写与异步数据流交互的代码。换句话说,你基本上是监听这些数据流中的新值(订阅数据流*),并根据这些值做出相应的响应。

想要深入了解响应式编程,可以看看这个链接这个链接。这两个链接都对(函数式)响应式编程的概念进行了很好的概述,并提供了一个非常清晰的思维模型。

Redux Observable 解决了哪些问题?

在选择新的库/工具/框架时,最重要的问题是它能如何帮助你完成工作。一般来说,Redux Observable 能做到的,Redux-Saga 也都能做到。它将你的逻辑从 action creator 中分​​离出来,在处理异步操作方面表现出色,而且易于测试。然而,就我个人而言,考虑到 Redux Observable 和 Redux Saga 的学习曲线都比较陡峭(生成器和响应式编程一开始都比较难掌握,因为它们不仅需要学习,还需要转变思维方式),Redux Observable 的整个工作流程用起来感觉更自然。

根据 Redux Observable 官方指南:这种处理副作用的模式类似于“进程管理器”模式(有时也称为“saga”),但 saga 的原始定义并不完全适用。如果您熟悉 redux-saga,那么 redux-observable 与之非常相似。但由于它使用 RxJS,因此更具声明性,您可以充分利用并扩展现有的 RxJS 功能。

我们现在可以开始编写代码了吗?

既然你已经了解了函数式响应式编程的概念,而且如果你和我一样,你会非常喜欢它处理数据的自然流畅感,那么现在就该将这个概念应用到你的 React/Redux 应用中了。

首先,和所有 Redux 中间件一样,在创建 store 时,必须将其添加到 Redux 应用程序中。

首先,要安装它,请运行
npm install --save rxjs rxjs-compat redux-observable

yarn add rxjs rxjs-compat redux-observable
根据您使用的工具运行。

Redux Observable 的基础是Epic。Epic类似于 Redux-Saga 中的 Saga,区别在于:Epic 不会等待 action 分发完毕后再将 action 委托给 worker,然后暂停执行直到收到另一个同类型的 action,而是独立运行并监听 action 流,并在收到流中的特定 action 时做出响应。Redux ActionsObservable-Observable 的主要组件是 Observable 组件,它继承Observable自 RxJS 的 Observable 组件。这个 Observable 组件表示一个 action 流,每次从应用程序中分发一个 action 时,该 action 都会被添加到这个流中。

好的,我们先来创建 Redux store 并添加 Redux Observable 中间件(友情提示:您可以使用create-react-appCLI 来启动 React 项目)。确认所有依赖项都已安装后redux, react-redux, rxjs, rxjs-compat, redux-observable,我们可以开始修改index.js文件,使其如下所示。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

import { createStore, applyMiddleware } from 'redux';
import { createEpicMiddleware } from 'redux-observable';
import { Provider } from 'react-redux';

const epicMiddleware = createEpicMiddleware(rootEpic);

const store = createStore(rootReducer, applyMiddleware(epicMiddleware));

const appWithProvider = (
    <Provider store={store}>
        <App />
    </Provider>
);

ReactDOM.render(appWithProvider, document.getElementById('root'));

您可能已经注意到,我们缺少了 `and`rootEpic和 ` rootReducer.`。别担心,我们稍后会添加它们。现在,让我们来看看这里发生了什么:

首先,我们导入创建 store 和应用中间件所需的必要函数。之后,我们使用createEpicMiddlewareRedux Observable 创建中间件,并将根 epic 传递给它(稍后会详细介绍)。然后,我们使用该createStore函数创建 store,并将根 reducer 传递给它,最后将 epic 中间件应用到 store 中。

好了,现在一切就绪,我们先来创建根 reducer。创建一个名为 `<root_reducer_name>` 的新文件夹reducers,并在其中创建一个名为 `<root_reducer_name>` 的新文件root.js。将以下代码添加到该文件中:

const initialState = {
    whiskies: [], // for this example we'll make an app that fetches and lists whiskies
    isLoading: false,
    error: false
};

export default function rootReducer(state = initialState, action) {
    switch (action.type) {
        default:
            return state;
    }
}

任何熟悉 Redux 的人都知道这里发生了什么——我们正在创建一个 reducer 函数,它接受 ` stateand` 和 `or`action作为参数,并根据 action 类型返回一个新的状态(因为我们还没有定义任何 action,所以我们只是添加default代码块并返回未修改的状态)。

现在,回到你的index.js文件,添加以下导入语句:

import rootReducer from './reducers/root';

如您所见,现在我们不再遇到文件rootReducer不存在的错误。现在让我们创建根史诗;首先,创建一个新文件夹epics,并在其中创建一个名为 `.epic.htm` 的文件index.js。暂时在该文件中添加以下代码:

import { combineEpics } from 'redux-observable';

export const rootEpic = combineEpics();

这里我们只是使用了combineEpicsRedux Observable 提供的函数来合并我们(目前还不存在的)epic,并将结果赋值给一个我们导出的常量。我们index.js现在应该通过添加以下导入语句来修复入口文件中的另一个错误:

import { rootEpic } from './epics';

太好了!现在我们已经完成了所有配置,接下来我们可以定义可以分发的操作类型,以及这些威士忌的操作创建器。

首先,创建一个名为 actions 的新文件夹,并index.js在其中创建一个文件。
(注意:对于大型生产级项目,您应该以逻辑方式对 actions、reducers 和 epics 进行分组,而不是全部放在一个文件中;但是,由于我们的应用程序非常小,因此这样做没有意义。)

在开始编写代码之前,我们先来思考一下可以分发哪些类型的 action。通常,我们需要一个 action 来通知 Redux/Redux-Observable 开始获取威士忌数据,我们把这个 action 称为 FETCH_WHISKIES。由于这是一个异步 action,我们无法确定它何时完成,因此我们需要在调用成功完成后分发一个 FETCH_WHISKIES_SUCCESS action。类似地,由于这是一个 API 调用,可能会失败,我们需要向用户发送一条消息来通知他们,因此我们会分发一个 FETCH_WHISKIES_FAILURE action 并通过显示错误消息来处理它。

让我们用代码定义这些操作(及其操作创建者):

export const FETCH_WHISKIES = 'FETCH_WHISKYS';
export const FETCH_WHISKIES_SUCCESS = 'FETCH_WHISKYS_SUCCESS';
export const FETCH_WHISKIES_FAILURE = 'FETCH_WHISKYS_FAILURE';

export const fetchWhiskies = () => ({
    type: FETCH_WHISKIES,
});

export const fetchWhiskiesSuccess = (whiskies) => ({
    type: FETCH_WHISKIES_SUCCESS,
    payload: whiskies
});

export const fetchWhiskiesFailure = (message) => ({
    type: FETCH_WHISKIES_FAILURE,
    payload: message
});

如果有人不清楚我在这里做什么,我只是简单地定义了 action 类型的常量,然后使用 ES6 的 lambda 简写形式创建箭头函数,这些函数返回一个包含 type 和(可选的)payload 属性的普通对象。type 用于标识分发的 action 类型,而 payload 则是分发 action 时向 reducer(和 store)发送数据的方式(注意:第二个属性不一定非得叫 payload,你可以随意命名,我这样命名只是为了保持一致性)。

现在我们已经创建了 action 和 action creator,接下来让我们在 reducer 中处理这些 action:
将您的代码更新reducers/index.js为以下内容。

import {
    FETCH_WHISKIES,
    FETCH_WHISKIES_FAILURE,
    FETCH_WHISKIES_SUCCESS
} from '../actions';

const initialState = {
    whiskies: [],
    isLoading: false,
    error: null
};

export default function rootReducer(state = initialState, action) {
    switch (action.type) {
        case FETCH_WHISKIES:
            return {
                ...state,
                // whenever we want to fetch the whiskies, set isLoading to true to show a spinner
                isLoading: true,
                error: null
            };
        case FETCH_WHISKIES_SUCCESS:
            return {
                whiskies: [...action.payload],
                // whenever the fetching finishes, we stop showing the spinner and then show the data
                isLoading: false,
                error: null
            };
        case FETCH_WHISKIES_FAILURE:
            return {
                whiskies: [],
                isLoading: false,
                // same as FETCH_WHISKIES_SUCCESS, but instead of data we will show an error message
                error: action.payload
            };
        default:
            return state;
    }
}

现在我们已经完成了所有这些工作,终于可以开始编写一些 Redux-Observable 代码了(抱歉耽搁了这么久!)

打开你的epics/index.js文件,让我们创建第一个史诗级任务。首先,你需要添加一些导入语句:

import { Observable } from 'rxjs';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import { ajax } from 'rxjs/observable/dom/ajax';

import {
    FETCH_WHISKIES,
    fetchWhiskiesFailure,
    fetchWhiskiesSuccess
} from "../actions";

这里我们导入了需要分发的 action 创建器、需要在 action 流中监听的 action 类型,以及一些 RxJS 的操作符Observable。请注意,RxJS 和 Redux Observable 都不会自动导入操作符,因此您需要手动导入它们(另一种方法是在入口 index.js 文件中导入整个 'rxjs' 模块,但我并不推荐这样做,因为它会导致包体积过大)。好的,接下来我们来看看这些导入的操作符及其作用:

map- 类似于 JavaScript 的原生方法Array.map()map它会对流中的每个元素执行一个函数,并返回一个包含映射元素的新流/Observable。- 它
of会根据一个非 Observable 值(可以是原始值、对象、函数等等)创建一个 Observable/流。-
ajax是 RxJS 提供的用于执行 AJAX 请求的模块;我们将使用它来调用 API。-
catch用于捕获可能发生的任何错误
switchMap。- 是其中最复杂的。它的作用是,它接受一个返回 Observable 的函数,并且每次这个内部 Observable 发出一个值时,它都会将该值合并到外部 Observable(调用 switchMap 的那个 Observable)中。但需要注意的是,每次创建一个新的内部 Observable 时,外部 Observable 都会订阅它(即监听值并将其合并到自身),并取消对先前发出值的所有其他 Observable 的订阅。这适用于我们不在乎先前结果是成功还是已被取消的情况。例如,当我们发送多个 action 来获取威士忌时,我们只需要最新的结果。`switchMap` 正是如此,它会订阅最新的结果,并将其合并到外部的 Observable 中,如果之前的请求尚未完成,则会将其丢弃。创建 POST 请求时,通常需要关注之前的请求是否已完成,这时就需要使用 `mergeMap`。`switchMap` 的mergeMap作用类似,但它不会取消订阅之前的 Observable。

考虑到这一点,让我们来看看获取威士忌的史诗级任务会是什么样子:

const url = 'https://evening-citadel-85778.herokuapp.com/whiskey/'; // The API for the whiskies
/*
    The API returns the data in the following format:
    {
        "count": number,
        "next": "url to next page",
        "previous": "url to previous page",
        "results: array of whiskies
    }
    since we are only interested in the results array we will have to use map on our observable
 */

function fetchWhiskiesEpic(action$) { // action$ is a stream of actions
    // action$.ofType is the outer Observable
    return action$
        .ofType(FETCH_WHISKIES) // ofType(FETCH_WHISKIES) is just a simpler version of .filter(x => x.type === FETCH_WHISKIES)
        .switchMap(() => {
            // ajax calls from Observable return observables. This is how we generate the inner Observable
            return ajax
                .getJSON(url) // getJSON simply sends a GET request with Content-Type application/json
                .map(data => data.results) // get the data and extract only the results
                .map(whiskies => whiskies.map(whisky => ({
                    id: whisky.id,
                    title: whisky.title,
                    imageUrl: whisky.img_url
                })))// we need to iterate over the whiskies and get only the properties we need
                // filter out whiskies without image URLs (for convenience only)
                .map(whiskies => whiskies.filter(whisky => !!whisky.imageUrl))
            // at the end our inner Observable has a stream of an array of whisky objects which will be merged into the outer Observable
        })
        .map(whiskies => fetchWhiskiesSuccess(whiskies)) // map the resulting array to an action of type FETCH_WHISKIES_SUCCESS
        // every action that is contained in the stream returned from the epic is dispatched to Redux, this is why we map the actions to streams.
        // if an error occurs, create an Observable of the action to be dispatched on error. Unlike other operators, catch does not explicitly return an Observable.
        .catch(error => Observable.of(fetchWhiskiesFailure(error.message)))
}

之后,还剩最后一件事,那就是将我们的史诗添加到combineEpics函数调用中,像这样:

export const rootEpic = combineEpics(fetchWhiskiesEpic);

好吧,这里面涉及的事情确实很多,我承认。但我们不妨把它一点一点地分析。

ajax.getJSON(url)返回一个 Observable,其流中的值是请求中的数据。
.map(data => data.results)从 Observable 中获取所有值(在本例中只有 1),results从响应中获取属性,并返回一个包含新值(即只有results数组)的新 Observable。

.map(whiskies => whiskies.map(whisky => ({
                    id: whisky.id,
                    title: whisky.title,
                    imageUrl: whisky.img_url
                })))

从前一个可观察对象(结果数组)中获取值,Array.map()对其进行调用,并将数组中的每个元素(每种威士忌)映射到一个新的对象数组,该数组仅保存每种威士忌的 id、标题和 imageUrl,因为我们不需要其他任何内容。

.map(whiskies => whiskies.filter(whisky => !!whisky.imageUrl))获取 Observable 中的数组,并返回一个包含过滤后数组的新 Observable。

封装此代码的函数switchMap会接收此 Observable,并将内部 Observable 的流合并到调用该函数的 Observable 的流中switchMap。如果再次收到威士忌的获取请求,则会再次重复此操作,并丢弃之前的结果,这要归功于该函数switchMap

.map(whiskies => fetchWhiskiesSuccess(whiskies))只需将我们添加到流中的这个新值映射到 FETCH_WHISKIES_SUCCESS 类型的操作,该操作将在 Observable 从 Epic 返回后分发。

.catch(error => Observable.of(fetchWhiskiesFailure(error.message)))它会捕获可能发生的任何错误,并返回一个 Observable 对象。然后,这个 Observable 对象会通过 switchMap 传递,并再次与外部 Observable 对象合并,最终我们在流中得到一个类型为 FETCH_WHISKIES_FAILURE 的操作。

请慢慢来,这是一个复杂的过程,如果您以前从未接触过响应式编程和 RxJS,它看起来和听起来可能会非常可怕(请阅读我上面提供的链接!)。

接下来,我们只需要渲染一个用户界面,其中包含一个用于触发操作的按钮和一个用于显示数据的表格。让我们开始吧;首先创建一个名为 components 的新文件夹,并在其中创建一个名为 Whisky.jsx 的新组件。

import React from 'react';

const Whisky = ({ whisky }) => (
    <div>
        <img style={{ width: '300px', height: '300px' }} src={whisky.imageUrl} />
        <h3>{whisky.title}</h3>
    </div>
);

export default Whisky;

这个组件只渲染一个威士忌产品,包括它的图片和标题。(拜托,千万别用内联样式。我在这里用内联样式只是因为这是一个简单的例子。)

现在我们要渲染一个威士忌元素的网格。让我们创建一个名为 WhiskyGrid.jsx 的新组件。

import React from 'react';

import Whisky from './Whisky';

const WhiskyGrid = ({ whiskies }) => (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr' }}>
        {whiskies.map(whisky => (<Whisky key={whisky.id} whisky={whisky} />))}
    </div>
);

export default WhiskyGrid;

WhiskyGrid 的作用是利用 CSS-Grid 创建一个每行 3 个元素的网格,它接收我们将作为 props 传入的 whiskies 数组,并将每种 whisky 映射到一个 Whisky 组件。

现在让我们来看一下 App.js 文件:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import './App.css';

import { fetchWhiskies } from './actions';

import WhiskyGrid from './components/WhiskyGrid';

class App extends Component {
  render() {
    const {
      fetchWhiskies,
      isLoading,
      error,
      whiskies
    } = this.props;

    return (
      <div className="App">
        <button onClick={fetchWhiskies}>Fetch whiskies</button>
        {isLoading && <h1>Fetching data</h1>}
        {!isLoading && !error && <WhiskyGrid whiskies={whiskies} />}
        {error && <h1>{error}</h1>}
      </div>
    );
  }
}

const mapStateToProps = state => ({ ...state });

const mapDispatchToProps = dispatch =>
    bindActionCreators({
        fetchWhiskies
    }, dispatch);

export default connect(mapStateToProps, mapDispatchToProps)(App);

如您所见,这里有很多修改。首先,我们需要将 Redux store 和 action creator 绑定到组件的 props 上。我们使用connectreact-redux 中的高阶组件 (HOC) 来实现这一点。之后,我们创建一个包含按钮的 div,该按钮的 onClick 事件设置为调用 fetchWhiskies action creator,现在已绑定到组件dispatch。点击按钮将分发 FETCH_WHISKIES action,我们的 Redux Observable epic 会获取到该 action,从而调用 API。接下来,我们设置了一个条件:如果 Redux store 中的 isLoading 属性为 true(FETCH_WHISKIES 已分发但尚未完成或抛出错误),则显示“加载数据”的文本。如果数据未加载且没有错误,则渲染组件WhiskyGrid并将 Redux 中的威士忌数据作为 prop 传递。如果 error 不为 null,则渲染错误消息。

结论

响应式编程并不容易。它提供了一种截然不同的编程范式,迫使你以不同的方式思考。我不会说函数式编程优于面向对象编程,也不会说响应式编程是最好的。在我看来,最好的编程范式是多种范式的结合。然而,我确实认为 Redux Observable 为其他异步 Redux 中间件提供了一个绝佳的替代方案。一旦你克服了学习曲线,你就会获得一种处理异步事件的绝妙而自然的方法。

如有任何疑问,请在评论区留言!如果大家对此感兴趣,我们可以考虑推迟或取消相关行动。

谢谢 :)

文章来源:https://dev.to/andrejnaumovski/async-actions-in-redux-with-rxjs-and-redux-observable-efg