Redux 实际上是用于管理 React 应用中状态的库。React 应用只需使用setState()
即可自行管理其组件的状态。这种方法的挑战在于没有任何东西可以控制状态更改的顺序(想想像 HTTP 请求这样的异步调用)。
本章的目的不是向您介绍 Redux,有很多相关资源,包括 Packt 书籍和官方 Redux 文档。因此,如果您是 Redux 新手,您可能需要花 30 分钟熟悉 Redux 的基本知识,然后再继续。本章的重点是您可以在 web 浏览器中启用的工具。我认为 Redux 的很大一部分价值来自 Redux DevTools 浏览器扩展。
在本章中,您将学习:
- 如何构建基本的 Redux 应用(无需深入了解 Redux 概念)
- 安装 Redux DevTools Chrome 扩展插件
- 选择 Redux 操作并检查其内容
- 如何使用时间旅行调试技术
- 手动触发操作以更改状态
- 导出应用状态并稍后导入
您将在本章中使用的示例应用是基本图书管理器。我们的目标是拥有足够的功能来演示不同的 Redux 操作,但足够简单,您可以学习 Redux 开发工具而不会感到不知所措。
此应用的高级功能如下所示:
- 呈现要跟踪的书籍列表。每本书都会显示书名、作者和封面图像。
- 允许用户通过键入文本输入来筛选列表。
- 用户可以创建一本新书。
- 用户可以选择一本书来查看更多详细信息。
- 书籍可以被删除。
在深入研究 Redux DevTools 扩展之前,让我们花几分钟的时间浏览一下这个应用的实现。
App
组件是 book manager 应用的外壳。您可以将App
视为每个其他渲染组件的容器。它负责呈现左侧导航,并定义应用的路由,以便在用户移动时安装和卸载适当的组件。以下是App
的实现情况:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
BrowserRouter as Router,
Route,
NavLink
} from 'react-router-dom';
import logo from './logo.svg';
import './App.css';
import Home from './Home';
import NewBook from './NewBook';
import BookDetails from './BookDetails';
class App extends Component {
render() {
const { title } = this.props;
return (
<Router>
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">{title}</h1>
</header>
<section className="Layout">
<nav>
<NavLink
exact
to="/"
activeStyle={{ fontWeight: 'bold' }}
>
Home
</NavLink>
<NavLink to="/new" activeStyle={{ fontWeight: 'bold' }}>
New Book
</NavLink>
</nav>
<section>
<Route exact path="/" component={Home} />
<Route exact path="/new" component={NewBook} />
<Route
exact
path="/book/:title"
component={BookDetails}
/>
</section>
</section>
</div>
</Router>
);
}
}
const mapState = state => state.app;
const mapDispatch = dispatch => ({});
export default connect(mapState, mapDispatch)(App);
react-redux
包中的connect()
函数用于将App
组件连接到 Redux 存储(应用状态所在的位置)。mapState()
和mapDispatch()
函数分别为App
组件状态值和动作调度器函数添加道具。到目前为止,App
组件只有一个状态值,没有动作调度器功能。
要更深入地了解如何将 React 组件连接到 Redux 商店,请查看以下页面:https://redux.js.org/basics/usage-with-react 。
让我们看看下面的减法函数:
const initialState = {
title: 'Book Manager'
};
const app = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default app;
除了一个title
之外,App
使用的状态没有太多。事实上,这title
从未改变。reducer 函数只返回传递给它的状态。您实际上不需要在这里使用switch
语句,因为这里没有要处理的操作。然而,title
状态很可能会随着你还不知道的行为而改变。像这样设置 reducer 函数从来都不是一个坏主意,这样您就可以将组件连接到 Redux 存储,这样一旦您确定了一个应该引起状态更改的操作,您就有了一个 reducer 函数来处理它。
Home
组件是作为App
的子组件呈现的第一个组件。Home
的路径是/
,这是过滤文本输入和图书列表的呈现位置。以下是用户首次加载应用时将看到的内容:
在左边,有两个导航链接,由App
组件呈现。在这些链接的右侧有过滤文本输入,后面是书籍列表。现在,让我们看一看 Oracle T1 组件实现:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchBooks } from '../api';
import Book from './Book';
import Loading from './Loading';
import './Home.css';
class Home extends Component {
componentWillMount() {
this.props.fetchBooks();
}
render() {
const {
loading,
books,
filterValue,
onFilterChange
} = this.props;
return (
<Loading loading={loading}>
<section>
<input
placeholder="Filter"
onChange={onFilterChange}
value={filterValue}
/>
</section>
<section className="Books">
{books
.filter(
book =>
filterValue.length === 0 ||
new RegExp(filterValue, 'gi').test(book.title)
)
.map(book => (
<Book
key={book.title}
title={book.title}
author={book.author}
imgURL={book.imgURL}
/>
))}
</section>
</Loading>
);
}
}
const mapState = state => state.home;
const mapDispatch = dispatch => ({
fetchBooks() {
dispatch({ type: 'FETCHING_BOOKS' });
fetchBooks().then(books => {
dispatch({
type: 'FETCHED_BOOKS',
books
});
});
},
onFilterChange({ target: { value } }) {
dispatch({ type: 'SET_FILTER_VALUE', filterValue: value });
}
});
export default connect(mapState, mapDispatch)(Home);
这里需要注意的关键事项是:
componentWillMount()
调用fetchBooks()
从 API 加载书籍数据Loading
组件用于在取书时显示加载的文本Home
组件定义了分派操作的函数,这是您希望使用 Redux DevTools 查看的- book 和 filter 数据来自 Redux 存储
以下是 reducer 函数,它处理操作并维护与此组件相关的状态:
const initialState = {
loading: false,
books: [],
filterValue: ''
};
const home = (state = initialState, action) => {
switch (action.type) {
case 'FETCHING_BOOKS':
return {
...state,
loading: true
};
case 'FETCHED_BOOKS':
return {
...state,
loading: false,
books: action.books
};
case 'SET_FILTER_VALUE':
return {
...state,
filterValue: action.filterValue
};
default:
return state;
}
};
export default home;
如果您查看initialState
对象,您可以看到Home
依赖于books
数组、filterValue
字符串和loading
布尔值。switch
语句中的每个动作案例都会改变此状态的一部分。虽然通过查看此 reducer 代码并结合 Redux 浏览器工具来破译正在发生的事情可能有点棘手,但图片变得清晰,因为您可以将在应用中看到的内容映射回此代码。
在左侧导航的主页链接下,有一个 NewBook 链接。单击此链接将带您进入允许您创建新书的表单。现在让我们来看看 PosiT00.组件源:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createBook } from '../api';
import './NewBook.css';
class NewBook extends Component {
render() {
const {
title,
author,
imgURL,
controlsDisabled,
onTitleChange,
onAuthorChange,
onImageURLChange,
onCreateBook
} = this.props;
return (
<section className="NewBook">
<label>
Title:
<input
autoFocus
onChange={onTitleChange}
value={title}
disabled={controlsDisabled}
/>
</label>
<label>
Author:
<input
onChange={onAuthorChange}
value={author}
disabled={controlsDisabled}
/>
</label>
<label>
Image URL:
<input
onChange={onImageURLChange}
value={imgURL}
disabled={controlsDisabled}
/>
</label>
<button
onClick={() => {
onCreateBook(title, author, imgURL);
}}
disabled={controlsDisabled}
>
Create
</button>
</section>
);
}
}
const mapState = state => state.newBook;
const mapDispatch = dispatch => ({
onTitleChange({ target: { value } }) {
dispatch({ type: 'SET_NEW_BOOK_TITLE', title: value });
},
onAuthorChange({ target: { value } }) {
dispatch({ type: 'SET_NEW_BOOK_AUTHOR', author: value });
},
onImageURLChange({ target: { value } }) {
dispatch({ type: 'SET_NEW_BOOK_IMAGE_URL', imgURL: value });
},
onCreateBook(title, author, imgURL) {
dispatch({ type: 'CREATING_BOOK' });
createBook(title, author, imgURL).then(() => {
dispatch({ type: 'CREATED_BOOK' });
});
}
});
export default connect(mapState, mapDispatch)(NewBook);
如果查看用于呈现此组件的标记,您将看到有三个输入字段。这些字段的值作为道具传递。与 Redux 商店的连接实际上就是这些道具的来源。当它们的状态改变时,NewBook
组件被重新渲染。
映射到此组件的分派功能负责分派维护此组件状态的操作。他们的职责如下:
onTitleChange()
:将SET_NEW_BOOK_TITLE
动作与新的title
状态一起发送onAuthorChange()
:将SET_NEW_BOOK_AUTHOR
动作与新的author
状态一起发送onImageURLChange()
:将SET_NEW_BOOK_IMAGE_URL
动作与新的imgURL
状态一起发送onCreateBook()
:当createBook()
API 调用返回时,发送CREATING_BOOK
动作,然后发送CREATED_BOOK
动作
如果不清楚所有这些操作是如何导致高级应用行为的,请不要担心。这就是为什么您将很快安装 Redux DevTools,以便了解应用状态发生变化时的情况。
以下是处理这些操作的 reducer 函数:
const initialState = {
title: '',
author: '',
imgURL: '',
controlsDisabled: false
};
const newBook = (state = initialState, action) => {
switch (action.type) {
case 'SET_NEW_BOOK_TITLE':
return {
...state,
title: action.title
};
case 'SET_NEW_BOOK_AUTHOR':
return {
...state,
author: action.author
};
case 'SET_NEW_BOOK_IMAGE_URL':
return {
...state,
imgURL: action.imgURL
};
case 'CREATING_BOOK':
return {
...state,
controlsDisabled: true
};
case 'CREATED_BOOK':
return initialState;
default:
return state;
}
};
export default newBook;
最后,这里是呈现新书形式时的样子:
当您填写这些字段并单击 Create 按钮时,新书将由 mock API 创建,您将被带回主页,在主页上应该列出新书。
对于这个应用,我使用了一个简单的 API 抽象。在 Redux 应用中,您应该能够将异步功能 API 或以其他方式封装在自己的模块或包中。以下是api.js
模块的外观,为了简洁起见,对一些模拟数据进行了编辑:
const LATENCY = 1000;
const BOOKS = [
{
title: 'React 16 Essentials',
author: 'Artemij Fedosejev',
imgURL: 'big long url...'
},
...
];
export const fetchBooks = () =>
new Promise(resolve => {
setTimeout(() => {
resolve(BOOKS);
}, LATENCY);
});
export const createBook = (title, author, imgURL) =>
new Promise(resolve => {
setTimeout(() => {
BOOKS.push({ title, author, imgURL });
resolve();
}, LATENCY);
});
export const fetchBook = title =>
new Promise(resolve => {
setTimeout(() => {
resolve(BOOKS.find(book => book.title === title));
}, LATENCY);
});
export const deleteBook = title =>
new Promise(resolve => {
setTimeout(() => {
BOOKS.splice(BOOKS.findIndex(b => b.title === title), 1);
resolve();
}, LATENCY);
});
要开始构建 Redux 应用,您只需要这些。这里需要注意的重要一点是,每个 API 函数都返回一个Promise
对象。为了更好地衡量,我添加了一些模拟延迟,因为这更像一个真实的 API。您不想让 API 抽象返回常规值,比如对象或数组。如果它们在与真实 API 交互时是异步的,请确保初始模拟也是异步的。否则,这是非常难以纠正的。
让我们快速看一下源文件,它们将所有内容组合在一起,给您一种完整感。让我们从index.js
开始:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Root from './components/Root';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<Root />, document.getElementById('root'));
registerServiceWorker();
这看起来就像您在本书中迄今为止使用的create-react-app
中的大多数index.js
文件一样。它不是渲染一个App
组件,而是渲染一个Root
组件。下面我们来看一下:
import React from 'react';
import { Provider } from 'react-redux';
import App from './App';
import store from '../store';
const Root = () => (
<Provider store={store}>
<App />
</Provider>
);
export default Root;
Root
的工作是用react-redux
中的Provider
组件包裹App
组件。此组件采用store
道具,这是您确保连接的组件能够访问 Redux 存储数据的方式。
下面让我们来看看下面的例子:
import { createStore } from 'redux';
import reducers from './reducers';
export default createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
Redux 有一个createStore()
功能,为你的 React 应用建立一个商店。第一个参数是 reducer 函数,它处理操作并返回存储的新状态。第二个参数是一个增强器函数,可以响应存储状态的更改。在这种情况下,您需要检查是否安装了 Redux DevTools 浏览器扩展,如果安装了,则将其连接到您的应用商店。如果没有此步骤,您将无法在 Redux 应用中使用浏览器工具。
我们差不多完成了。让我们看一下reducers/index.js
文件,它将您的 reducer 函数组合成一个函数:
import { combineReducers } from 'redux';
import app from './app';
import home from './home';
import newBook from './newBook';
import bookDetails from './bookDetails';
const reducers = combineReducers({
app,
home,
newBook,
bookDetails
});
export default reducers;
Redux 只有一个商店。为了将存储细分为映射到应用概念的状态片,您可以命名处理不同状态片的各个 reducer 函数,并将它们传递给combineReducers()
。使用此应用,您的商店可以将以下状态片段映射到组件:
app
home
newBook
bookDetails
现在,您已经了解了这个应用是如何组合起来的以及它是如何工作的,现在是时候开始使用 Redux DevTools 浏览器扩展对其进行检测了。
安装 Redux DevTools 浏览器扩展遵循与安装 React Developer Tools 扩展类似的过程。第一步是打开 Chrome 网络商店并搜索redux
:
您正在寻找的扩展可能是第一个结果:
继续并单击添加到 Chrome 按钮。然后,您将看到一个对话框,在显示扩展可以更改的内容后,该对话框要求您允许安装扩展:
单击“添加扩展”按钮后,您将看到一条通知,说明已安装扩展:
与 React Developer 工具扩展一样,Redux DevTools 图标将保持禁用状态,直到您打开运行 Redux 并添加了对该工具的支持的页面。回想一下,您使用以下代码在 book manager 应用中明确添加了对此工具的支持:
export default createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
现在,让我们启动 book manager 应用,确保您可以使用该扩展。在运行npm start
并等待 UI 在浏览器选项卡中打开和加载后,应同时启用 React 和 Redux developer 工具图标:
接下来,打开“开发人员工具”浏览器窗格。访问 Redux DevTools 的方式与访问 React Developer 工具的方式相同:
当您选择 Redux 工具时,您应该会看到类似的内容:
Redux DevTools 中的左窗格包含应用中操作的最重要数据。正如这里所反映的,您的 book manager 应用已发送了三个操作,因此您知道一切正常!
Redux DevTools 左侧窗格中显示的操作按时间顺序列出,具体时间取决于它们被调度的时间。可以选择任何操作,通过这样做,您可以使用右侧窗格检查应用状态和操作本身的不同方面。在本节中,您将了解如何更深入地了解 Redux 操作是如何驱动应用的。
通过选择操作,可以查看作为操作一部分发送的数据。但首先,让我们生成一些操作。一旦应用加载,FETCHING_BOOKS
和FETCHED_BOOKS
动作被调度。单击 React Native Blueprints 链接,该链接加载图书数据并将您带到图书详细信息页面。这导致两个新动作被调度:FETCHING_BOOK
和FETCHED_BOOK
。呈现的 React 内容应如下所示:
Redux DevTools 中的操作列表应如下所示:
@@INIT
操作由 Redux 自动调度,并且始终是第一个操作。通常,您不需要担心这个操作,除非您需要在分派和操作之前知道应用的状态,我们将在下一节介绍这些操作。
现在,让我们选择FETCHING_BOOKS
动作。然后,在右侧窗格中,选择“动作切换”按钮以查看动作数据。您应该看到如下内容:
默认情况下,将选择操作的树视图。您可以在这里看到,动作数据有一个名为type
的属性,其值是动作的名称。这告诉您,reducer 应该知道如何处理此操作,并且不需要任何附加数据。
现在我们选择FETCHED_BOOKS
动作,看看动作数据是什么样子的:
再一次,您拥有带有操作名称的type
属性。这一次,您还拥有一个包含一系列书籍的books
属性。此操作被调度以响应 API 数据解析以及书本数据如何通过操作进入存储区。
通过查看操作数据,您可以比较实际调度的内容与您在应用状态中看到的内容。更改应用状态的唯一方法是使用新状态分派操作。接下来,让我们看看单个操作如何改变应用的状态。
在上一节中,您了解了如何使用 Redux DevTools 选择特定操作以查看其数据。操作及其携带的数据会导致应用状态的更改。选择操作时,可以查看该操作对整个应用状态的影响。
让我们选择FETCHING_BOOK
操作,然后选择右侧窗格中的状态切换按钮:
此树状图显示了FETCHING_BOOK
操作发出后应用的整个状态。bookDetails
状态在此展开,以便您可以看到操作对状态的影响。在这种情况下,loading
值现在是true
。
现在,让我们选择此操作的图表视图:
对于可视化应用的整个状态,我恰好更喜欢图表视图而不是树视图。在图表的最左边是根状态。在此右侧,您有应用状态的主要部分-app
、home
、newBook
和bookDetails
。随着你越来越向右移动,你正在深入了解应用中组件的特定状态。正如您在这里看到的,最深层是books
数组中的单个书籍,这是home
状态的一部分。
FETCHING_BOOK
动作仍处于选中状态,这意味着此图表反映了减速器响应此动作后的应用状态。此动作改变bookDetails
内的loading
状态。如果将鼠标指针移到状态标签上,将看到其值:
现在让我们选择FETCHED_BOOK
动作。当通过 API 调用解析图书详细信息数据时,将调度此操作:
如果在切换到其他操作时保持图表视图处于激活状态,您会注意到图表实际上会为状态的更改设置动画。毫无疑问,它看起来很酷,但它也会让你注意到实际发生变化的值,以便更容易看到它们。在本例中,如果您查看bookDetails
下的book
对象,您将看到它现在具有新属性。您可以将鼠标指针移动到它们上面,以显示它们的值。您也可以检查loading
值,它应该回到false
。
在 Redux DevTools 中查看操作数据的另一种方法是查看调度操作所导致的状态差异。与通过查看整个状态树来收集状态的变化不同,此视图只显示发生了哪些变化。
让我们尝试添加一本新书来生成一些操作。我要加上你现在正在读的那本书。首先,我将粘贴在书的标题中,该书名在输入元素上生成一个变更事件,该事件反过来发送一个SET_NEW_BOOK_TITLE
动作。如果选择该操作,则应看到以下内容:
newBook
状态的title
值从空字符串变为粘贴到标题文本输入中的值。不必去寻找这个变化,它被清晰地标记出来让你们看,所有不相关的状态数据都隐藏在视图中。
接下来,让我们粘贴作者并选择SET_NEW_BOOK_AUTHOR
动作:
再一次,这里只显示了author
值,因为它是由于调度SET_NEW_BOOK_AUTHOR
而更改的唯一值。以下是图像 URL 的最终表单字段:
通过使用操作的 Diff 视图,您只能看到由于操作而更改的数据。如果这不能提供足够的透视图,您可以随时跳转到状态视图,以便查看整个应用的状态。
让我们通过单击 create 按钮来创建新书。这将发送两个动作:CREATING_BOOK
和CREATED_BOOK
。首先来看CREATING_BOOK
:
此操作在调用创建书籍之前发出。这使您的 React 组件有机会处理用户交互的异步性质。在这种情况下,您不希望用户在请求挂起时能够与任何表单控件交互。通过查看此差异,您可以看到,controlsDisabled
值现在是false
,React 组件可以使用该值禁用任何表单控件。
最后,我们来看一下CREATED_BOOK
动作:
title
、author
和imgURL
值被设置为空字符串,这将重置表单字段值。表单字段也通过将controlsDisabled
设置为false
重新启用。
Redux 中 reducer 函数的一个要求是它们是纯的;也就是说,它们只返回新数据,而不是改变现有数据。这样做的一个结果是,它支持时间旅行调试。因为没有任何变化,所以可以将应用的状态向前、向后或移动到任意时间点。Redux 开发工具使这一点很容易做到。
要查看时间旅行调试的运行情况,让我们在过滤器输入框中键入一些过滤器文本:
查看 Redux DevTools 中的操作,您应该会看到以下内容:
我已选择上次发送的SET_FILTER_VALUE
操作。filterValue
值应为native b
,反映当前显示的标题。现在,让我们回到两个动作之前。要执行此操作,请将鼠标指针移到当前选定操作后面两个位置的操作上。单击跳转按钮,应用的状态将更改为发送此SET_FILTER_VALUE
时的状态:
您可以看到filterValue
已经从native b
变为native
。您已经有效地撤消了最后两次击键,并相应地更新了状态和 UI:
要将应用状态恢复到当前时间,请执行相同的过程,但顺序相反。点击跳转到最近的状态。
在开发 Redux 应用期间手动触发操作的能力可能会有所帮助。例如,您可能已经准备好了组件,但是您不确定用户交互将如何工作,或者您只需要对应该工作但不工作的部分进行故障排除。您可以使用 Redux DevTools 手动触发操作,方法是单击窗格底部附近带有键盘图标的按钮:
这将显示一个文本输入,您可以在其中输入操作负载。例如,我已导航到 React Native By example 的书籍详细信息页面:
我不想单击 Delete 按钮,只想查看应用的状态,而不触发 DOM 事件或 API 调用。要做到这一点,我可以单击 Redux DevTools 中的键盘按钮,它允许我手动输入一个操作并发送它。例如,我将如何发送DELETING_BOOK
操作:
这将导致调度操作,从而更新 UI。以下是DELETING_BOOK
行动:
要将controlsDisabled
设置回false
,您可以发送DELETED_BOOK
动作:
随着 Redux 应用的大小和复杂度的增长,状态树的大小和复杂度将随之增长。正因为如此,有时会玩弄个人动作,让你的应用进入特定状态可能过于繁琐,无法一次又一次地手动执行。
使用 Redux DevTools,可以导出应用的当前状态。然后,当您稍后进行故障排除时,需要一个特定的状态作为起点,您可以直接加载它,而不是手动重新创建它。
让我们尝试导出应用状态。首先,导航到 React 16 Essentials 的详细信息页面:
要使用 Redux DevTools 导出当前状态,请单击带有向下箭头的按钮:
然后,可以使用向上箭头导入状态。但在此之前,请浏览到其他书名,例如《快速入门:
现在,您可以使用 Redux DevTools 窗格中的上载按钮:
由于您已经在“书本详细信息”页面上,加载此状态将替换此页面上组件呈现的状态值:
现在您知道如何将 Redux 存储的状态恢复到您在本地导出和保存的任何给定点。这样做的目的是避免为了达到特定状态而必须记住并按照正确的顺序执行纠正操作。这很容易出错,导出所需的确切状态可以避免整个过程。
在本章中,您将组装一个简单的 book manager Redux 应用。应用安装到位后,您就学会了如何在 Chrome 中安装 Redux DevTools 浏览器扩展。从那里,您学习了如何查看和选择操作。
选择操作后,可以通过多种方式查看有关应用的信息。您可以查看操作负载数据。您可以查看整个应用状态。您可以查看应用状态与上次调度的操作之间的差异。这些都是可以用来测试 Redux 应用的不同方法。
然后,您了解了时间旅行调试如何在 Redux DevTools 中工作。因为状态更改在 Redux 中是不可变的,所以您可以使用 Redux DevTools 从一个操作跳到另一个操作。这可以大大简化调试周期。最后,您学习了如何手动分派操作和导入/导出应用的状态。
在下一章中,您将学习如何使用盖茨比从 React 组件生成静态内容。