redux-observable

什么是Redux?

Redux是一个了不起的库。对于那些不知道Redux是什么的人来说,它是JavaScript应用程序的可预测状态容器。在应用中,它可以作为应用程序状态的单一事实来源。调用状态或Redux_存储只能通过调度操作进行更改,调度操作_由 reducers处理,后者根据调度的操作类型决定如何修改状态。对于那些不熟悉Redux的人,请查看此链接

现在,Redux最常与React结合使用,虽然它不受它约束 - 它可以与任何其他视图库一起使用。

Redux的问题

但是,Redux有一个非常重要的问题 - 它本身并不能很好地处理异步操作。这很糟糕,但另一方面,Redux只是一个库,为您的应用程序提供状态管理,就像React只是一个视图库一样。这些都不构成一个完整的框架,您必须自己选择其他的工具。有些人认为这是一件坏事,因为没有一种指定的规范,包括我在内的一些人认为它很好,因为你不受任何特定技术的束缚。这很好,因为每个人都可以选择他们认为最符合他们需求的技术。

处理异步操作

现在,有几个库提供Redux中间件来处理异步操作。当我第一次开始使用React和Redux时,我被分配的项目使用了Redux-Thunk。Redux-Thunk允许您编写返回函数而不是普通对象的动作创建者(默认情况下,Redux中的所有操作都必须是普通对象),这反过来又允许您延迟调度某些操作。

作为当时React / Redux的初学者,thunk非常棒。它们易于编写和理解,并且不需要任何其他功能 - 您基本上只是以不同的方式编写动作创建者。

但是,一旦你开始使用React和Redux进入工作流程,你就会意识到,虽然很容易使用,但是thunks并不是那么好,因为,1。你最终可能会回调地狱,特别是在发出API请求时,2。您要么使用业务逻辑填充回调函数或减少函数来处理数据(因为,老实说,您不会每次都获得格式完美的数据,特别是如果您使用第三方API),以及3.它们不是真正可测试的(你必须使用间谍方法来检查是否已使用正确的对象调用了调度)。所以,我开始研究其他可能更适合的解决方案。那是我遇到Redux-Saga的时候

Redux Saga非常接近我想要的东西。你能感觉它就像一个单线程的应用一样,它独自负责副作用。这基本上意味着sagas与主应用程序分开运行并监听调度操作 - 一旦调度该特定saga正在侦听的操作,它就会执行一些产生副作用的代码,如API调用。它还允许你从saga内部dispatch其他action,而且很容易测试,因为sagas返回效果_是普通对象。听起来不错,对吗?

Redux-Saga为大多数开发人员提供了折中,也是一个很大的折中方案 - 它利用了Javascript的generator功能,这些功能具有相当陡峭的学习曲线。Redux Saga创作者使用JS这个强大的功能,但是,我觉得generator功能使用起来感觉很不自然,至少对我来说,即使我知道如何他们工作以及如何使用它们,我只是无法实际使用它们。就像那个乐队或歌手一样,当他们在收音机上播放时你听起来并没有什么问题,但是你甚至不会考虑自己演奏它们。这就是为什么我继续搜索异步处理Redux中间件的原因。

Redux-Saga不能很好地处理的另一件事是取消已经调度的异步操作 - 例如API调用(Redux Observable由于其响应特性而做得非常好)。

下一步

大约一个星期前,我正在看一个朋友和我为大学写过的旧Android项目,并在那里看到了一些RxJava代码,并自言自语:如果有一个Redux的Reactive中间件怎么办?所以我做了一些研究,好吧,众神听到了我的祈祷:Cue Redux Observable

那么什么 Redux Observable?它是Redux的另一个中间件,它允许您以功能,反应和声明的方式处理异步数据流。这是什么意思?这意味着您编写的代码适用于异步数据流。换句话说,您基本上会在这些流上监听新值(订阅流*)并相应地对这些值做出反应。

有关响应式编程的最深入的指南,请查看此链接此链接。两者都对(功能)响应式编程提供了非常好的概述,并为您提供了一个非常好的入门。

Redux Observable解决了哪些问题?

在查看新的库/工具/框架时,最重要的问题是它如何帮助您完成工作。一般来说,Redux Observable所做的一切,Redux-Saga也是如此。它将您的逻辑移动到您的动作创建者之外,它在处理异步操作方面表现出色,并且易于测试。然而,在我的观点中,Redux Observable的整个工作流程感觉更自然,考虑到这两者都有一个陡峭的学习曲线(generator和响应式编程起初有点难以掌握,因为它们不仅需要学习而且还需要学习适应你的思维方式)。

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

所以,既然你知道什么是功能性反应式编程,如果你像我一样,你真的很喜欢操作数据的感觉。是时候将这个概念应用到您的React / Redux应用程序了。

首先,作为任何Redux中间件,您必须在创建商店时将其添加到Redux应用程序中。

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

yarn add rxjs rxjs-compat redux-observable
取决于您正在使用的工具。

现在,Redux Observable的基础是Epics。Epics与Redux-Saga中的sagas相似,不同之处在于,不是等待动作调度并将动作委托给worker,而是暂停执行,直到使用yield关键字进行相同类型的另一个动作,epics分别运行并且听取一系列动作,然后在流上收到特定动作时作出反应。主要组件是ActionsObservableRedux-Observable,它扩展了ObservableRxJS。此observable表示一系列操作,每次从应用程序发送操作时,都会将其添加到流中。

好的,让我们首先创建我们的Redux存储并向其添加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'));

你可能已经注意到了,我们错过了rootEpicrootReducer。不要担心这个,我们稍后会添加它们。现在,让我们来看看这里发生了什么:

首先,我们正在导入必要的功能来创建我们的商店并应用我们的中间件。在那之后,我们使用createEpicMiddlewareRedux Observable创建我们的中间件,并将其传递给根epics(我们将在稍后介绍)。然后我们使用该createStore函数创建我们的store并将其传递给root redurcer并将epics中间件应用于store。

好的,现在我们已经完成了所有设置,让我们首先创建我们的root reducer。创建一个新文件夹reducers,其中包含一个名为的新文件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函数,它接受stateaction作为参数,并根据动作类型返回一个新状态(因为我们还没有定义任何动作,我们只是添加的default块,并返回初始化状态)。

现在,返回到您的index.js文件并添加以下导入:

import rootReducer from './reducers/root';

如您所见,现在我们没有关于rootReducer不存在的错误。现在让我们创造我们的root epics; 首先,创建一个新文件夹epics并在其中创建一个名为的文件index.js。在其中,暂时添加以下代码:

import { combineEpics } from 'redux-observable';

export const rootEpic = combineEpics();

这里我们只是使用combineEpicsRedux Observable 提供的函数来组合我们的(现在,不存在)epics,并将该值赋给我们导出的常量。我们现在应该index.js通过简单地添加以下导入来修复条目文件中的其他错误:

import { rootEpic } from './epics';

不错,现在我们处理了所有配置,我们可以定义我们可以dispatch的action类型以及这些action creator。

首先,index.js在里面创建一个名为actions和一个文件的新文件夹。
(注意:对于大型的生产级的项目,您应该以合理的方式对action,reducer和epics进行分组,而不是将它们全部放在一个文件中,但是,由于我们的应用程序非常小,因此没有任何意义)

在我们开始编写代码之前,让我们考虑一下我们可以调度的action类型。通常,我们需要一个action来通知Redux / Redux-Observable它应该开始获取epics,让我们调用该动作FETCH_WHISKIES。由于这是一个异步操作,我们不知道它究竟何时完成,因此我们将希望在调用成功完成时调度FETCH_WHISKIES_SUCCESS操作。以类似的方式,由于这是一个API调用,它可能会失败,我们希望通过消息通知我们的用户,因此我们将调度FETCH_WHISKIES_FAILURE操作并通过显示错误消息来处理它。

让我们在代码中定义这些动作(及其动作创建者):

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
});

对于那些不清楚我在这里做什么的人,我只是为动作类型定义常量,然后使用ES6的lambda简写符号我创建箭头函数,返回包含类型和(可选)普通对象属性。该类型用于标识已分派的操作类型,有效负载是在调度操作时将数据发送到Reducer(和存储)的方式。

现在我们已经创建了我们的action和action creator,让我们在reducer中处理这些动作:
更新您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 crator以及我们需要在动作流中观察的action type,以及来自RxJS的一些操作符以及Observable。请注意,RxJS和Redux Observable都不会自动导入运算符,因此您必须自己导入它们(另一种选择是在您的条目index.js中导入整个'rxjs'模块,但我不建议这样做,因为它会给你大束尺寸)。好的,让我们来看看我们导入的这些运算符以及它们的作用:

map- 类似于Javascript的本机Array.map()map在流中的每个项目上执行一个函数,并返回带有映射项目的新流/ Observable。
of - 从非Observable值创建一个Observable / stream(它可以是一个原语,一个对象,一个函数,任何东西)。
ajax - 是提供的用于执行AJAX请求的RxJS模块; 我们将使用它来调用API。
catch - 用于捕获可能发生的任何错误
switchMap - 这是最复杂的。它的作用是,它接受一个返回Observable的函数,每次这个内部Observable发出一个值时,它会将该值合并到外部Observable(调用switchMap的那个)。这是捕获,每次创建一个新的内部Observable时,外部Observable都会订阅它(即侦听值并将它们合并到自身),并取消对先前发出的Observable的所有其他订阅。这对于我们不关心先前结果是否已成功或已被取消的情况非常有用。例如,当我们发送多个用于获取威士忌的操作时,我们只需要最新的结果,switchMap就是这样做的,它将订阅最新的结果并将其合并到外部Observable并丢弃之前的请求(如果它们仍未完成)。在创建POST请求时,您通常关心先前的请求是否已完成,以及是否使用了mergeMap。mergeMap 做同样的事情,除非它没有取消先前的Observables。

考虑到这一点,让我们看看用于获取威士忌的Epic将如何显示:

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从响应中获取属性并返回带有新值的新Observable(即只有results数组)。

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

从前一个observable(结果数组)获取值,调用Array.map()它,并映射数组的每个元素(每个威士忌)以创建一个新的对象数组,只保存每个威士忌的id,title和imageUrl,因为我们不需要别的。

.map(whiskies => whiskies.filter(whisky => !!whisky.imageUrl)) 获取Observable中的数组并返回带有已过滤数组的新Observable。

switchMap一个包装此代码借此观察到的,内可观测的流合并到调用可观察到的数据流switchMap。如果另一个威士忌提取请求通过,则此操作将再次重复,之前的结果将被丢弃,这要归功于switchMap

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

.catch(error => Observable.of(fetchWhiskiesFailure(error.message)))捕获可能发生的任何错误,只返回一个Observable。然后,此Observable通过switchMap传播,再次将其合并到外部Observable,我们在流中获得类型为FETCH_WHISKIES_FAILURE的操作。

花点时间,这是一个复杂的过程,如果你还没有碰过Reactive编程和RxJS,它看起来和声音都非常可怕(阅读我上面提供的那些链接!)。

在此之后,我们需要做的就是渲染一个UI,它将有一个用于调度操作的按钮和一个用于显示数据的表。我们这样做; 首先创建一个名为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个元素的网格,只需将威士忌数组作为道具传入,并将每个威士忌映射到威士忌组件。

现在让我们来看看我们的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 class>
        <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存储和动作创建器绑定到组件的props。我们使用connectreact-redux中的HOC来实现这一目标。之后,我们创建一个div,它有一个按钮,其onClick设置为调用fetchWhiskies动作创建者,现在绑定到dispatch。单击该按钮将调度FETCH_WHISKIES操作,我们的Redux Observable epic将拾取它,从而调用API。接下来我们有一个条件,如果Redux存储中的isLoading属性为true(已调度FETCH_WHISKIES但既未完成也未抛出错误),我们将显示一条文本,说明加载数据。如果数据未加载且没有错误,我们将渲染WhiskyGrid组件并将Redux中的威士忌作为道具传递。如果error不为null,则呈现错误消息。

结论

响应式并不容易。它提出了一种完全不同的编程范式,它迫使你以不同的方式思考。我不会说功能性比面向对象更好,或者说Reactive是最好的。最好的编程范式,IN MY OPINION,是范式的组合。但是,我相信Redux Observable确实提供了其他异步Redux中间件的绝佳替代方案,在您通过学习曲线之后,您将获得一种处理异步事件的神奇,自然的方法。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容