什么是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分别运行并且听取一系列动作,然后在流上收到特定动作时作出反应。主要组件是ActionsObservable
Redux-Observable,它扩展了Observable
RxJS。此observable表示一系列操作,每次从应用程序发送操作时,都会将其添加到流中。
好的,让我们首先创建我们的Redux存储并向其添加Redux Observable中间件(小提醒,引导可以使用create-react-app
CLI 的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'));
你可能已经注意到了,我们错过了rootEpic
和rootReducer
。不要担心这个,我们稍后会添加它们。现在,让我们来看看这里发生了什么:
首先,我们正在导入必要的功能来创建我们的商店并应用我们的中间件。在那之后,我们使用createEpicMiddleware
Redux 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函数,它接受state
和action
作为参数,并根据动作类型返回一个新状态(因为我们还没有定义任何动作,我们只是添加的default
块,并返回初始化状态)。
现在,返回到您的index.js
文件并添加以下导入:
import rootReducer from './reducers/root';
如您所见,现在我们没有关于rootReducer
不存在的错误。现在让我们创造我们的root epics; 首先,创建一个新文件夹epics
并在其中创建一个名为的文件index.js
。在其中,暂时添加以下代码:
import { combineEpics } from 'redux-observable';
export const rootEpic = combineEpics();
这里我们只是使用combineEpics
Redux 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。我们使用connect
react-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中间件的绝佳替代方案,在您通过学习曲线之后,您将获得一种处理异步事件的神奇,自然的方法。