Redux 基础知识,第 4 部分:存储
如何创建 Redux 存储
¥How to create a Redux store
如何使用 store 更新状态并监听更新
¥How to use the store to update state and listen for updates
如何配置存储以扩展其功能
¥How to configure the store to extend its capabilities
如何设置 Redux DevTools 扩展来调试你的应用
¥How to set up the Redux DevTools Extension to debug your app
介绍
¥Introduction
在 第 3 部分:状态、操作和 reducer 年,我们开始编写示例待办事项应用。我们列出了业务需求,定义了应用运行所需的状态结构,并创建了一系列操作类型来描述 "发生了什么" 并匹配用户与应用交互时可能发生的事件类型。我们还编写了可以处理更新 state.todos
和 state.filters
部分的 reducer 函数,并了解了如何使用 Redux combineReducers
函数根据应用中每个功能的不同 "切片 reducer" 创建 "根 reducer"。
¥In Part 3: State, Actions, and Reducers, we started writing our example todo app. We
listed business requirements, defined the state structure we need to make the app work, and created a series of action types
to describe "what happened" and match the kinds of events that can happen as a user interacts with our app. We also wrote reducer functions that can handle updating our state.todos
and state.filters
sections, and saw how we can use the Redux combineReducers
function
to create a "root reducer" based on the different "slice reducers" for each feature in our app.
现在,是时候将这些部分与 Redux 应用的核心部分整合在一起了:存储。
¥Now, it's time to pull those pieces together, with the central piece of a Redux app: the store.
请注意,本教程有意展示旧式 Redux 逻辑模式,这些模式比我们使用 Redux Toolkit 教授的 "现代回归" 模式需要更多的代码,作为当今使用 Redux 构建应用的正确方法,以便解释 Redux 背后的原理和概念。它并不意味着是一个生产就绪的项目。
¥Note that this tutorial intentionally shows older-style Redux logic patterns that require more code than the "modern Redux" patterns with Redux Toolkit we teach as the right approach for building apps with Redux today, in order to explain the principles and concepts behind Redux. It's not meant to be a production-ready project.
请参阅以下页面以了解如何将 "现代回归" 与 Redux Toolkit 结合使用:
¥See these pages to learn how to use "modern Redux" with Redux Toolkit:
完整的 "Redux 要点" 教程,它使用 Redux Toolkit 教授 "如何正确使用 Redux" 实际应用。我们建议所有 Redux 学习者都应该阅读 "必需品" 教程!
¥The full "Redux Essentials" tutorial, which teaches "how to use Redux, the right way" with Redux Toolkit for real-world apps. We recommend that all Redux learners should read the "Essentials" tutorial!
Redux 基础知识,第 8 部分:使用 Redux 工具包的现代 Redux,展示了如何将前面部分的底层示例转换为现代 Redux Toolkit 等效项
¥Redux Fundamentals, Part 8: Modern Redux with Redux Toolkit, which shows how to convert the low-level examples from earlier sections into modern Redux Toolkit equivalents
Redux 存储
¥Redux Store
Redux 存储汇集了构成应用的状态、操作和化简器。存储有几项职责:
¥The Redux store brings together the state, actions, and reducers that make up your app. The store has several responsibilities:
内部保存当前应用状态
¥Holds the current application state inside
允许通过
store.getState()
访问当前状态;¥Allows access to the current state via
store.getState()
;允许通过
store.dispatch(action)
更新状态;¥Allows state to be updated via
store.dispatch(action)
;通过
store.subscribe(listener)
注册监听器回调;¥Registers listener callbacks via
store.subscribe(listener)
;通过
store.subscribe(listener)
返回的unsubscribe
函数处理监听器的取消注册。¥Handles unregistering of listeners via the
unsubscribe
function returned bystore.subscribe(listener)
.
请务必注意,Redux 应用中只有一个存储。当你想要拆分数据处理逻辑时,你将使用 reducer 组合物 并创建多个可以组合在一起的 reducer,而不是创建单独的存储。
¥It's important to note that you'll only have a single store in a Redux application. When you want to split your data handling logic, you'll use reducer composition and create multiple reducers that can be combined together, instead of creating separate stores.
创建存储
¥Creating a Store
每个 Redux 存储都有一个根 reducer 函数。在上一节中,我们 使用 combineReducers
创建了一个根 reducer 函数。该根 reducer 当前在我们示例应用的 src/reducer.js
中定义。让我们导入根 reducer 并创建我们的第一个存储。
¥Every Redux store has a single root reducer function. In the previous section, we created a root reducer function using combineReducers
. That root reducer is currently defined in src/reducer.js
in our example app. Let's import that root reducer and create our first store.
Redux 核心库有 createStore
API 将创建存储。添加一个名为 store.js
的新文件,并导入 createStore
和根 reducer。然后,调用 createStore
并传入根 reducer:
¥The Redux core library has a createStore
API that will create the store. Add a new file
called store.js
, and import createStore
and the root reducer. Then, call createStore
and pass in the root reducer:
import { createStore } from 'redux'
import rootReducer from './reducer'
const store = createStore(rootReducer)
export default store
加载初始状态
¥Loading Initial State
createStore
还可以接受 preloadedState
值作为其第二个参数。你可以使用它在创建存储时添加初始数据,例如从服务器发送的 HTML 页面中包含的值,或者保留在 localStorage
中并在用户再次访问该页面时读回的值,如下所示:
¥createStore
can also accept a preloadedState
value as its second argument. You could use this to add
initial data when the store is created, such as values that were included in an HTML page sent from the server, or persisted in
localStorage
and read back when the user visits the page again, like this:
import { createStore } from 'redux'
import rootReducer from './reducer'
let preloadedState
const persistedTodosString = localStorage.getItem('todos')
if (persistedTodosString) {
preloadedState = {
todos: JSON.parse(persistedTodosString)
}
}
const store = createStore(rootReducer, preloadedState)
调度动作
¥Dispatching Actions
现在我们已经创建了一个存储,让我们验证我们的程序是否有效!即使没有任何 UI,我们也可以测试更新逻辑。
¥Now that we have created a store, let's verify our program works! Even without any UI, we can already test the update logic.
在运行此代码之前,请尝试返回 src/features/todos/todosSlice.js
,并从 initialState
中删除所有示例待办事项对象,使其成为空数组。这将使该示例的输出更容易阅读。
¥Before you run this code, try going back to src/features/todos/todosSlice.js
, and remove all the example todo objects from the initialState
so that it's an empty array. That will make the output from this example a bit easier to read.
// Omit existing React imports
import store from './store'
// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
console.log('State after dispatch: ', store.getState())
)
// Now, dispatch some actions
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' })
store.dispatch({ type: 'todos/todoToggled', payload: 0 })
store.dispatch({ type: 'todos/todoToggled', payload: 1 })
store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' })
store.dispatch({
type: 'filters/colorFilterChanged',
payload: { color: 'red', changeType: 'added' }
})
// Stop listening to state updates
unsubscribe()
// Dispatch one more action to see what happens
store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })
// Omit existing React rendering logic
请记住,每次我们调用 store.dispatch(action)
时:
¥Remember, every time we call store.dispatch(action)
:
存储调用
rootReducer(state, action)
¥The store calls
rootReducer(state, action)
该根 reducer 可能会在其内部调用其他切片 reducer,例如
todosReducer(state.todos, action)
¥That root reducer may call other slice reducers inside of itself, like
todosReducer(state.todos, action)
存储将新的状态值保存在里面
¥The store saves the new state value inside
store 调用所有监听订阅回调
¥The store calls all the listener subscription callbacks
如果监听器可以访问
store
,它现在可以调用store.getState()
来读取最新的状态值¥If a listener has access to the
store
, it can now callstore.getState()
to read the latest state value
如果我们查看该示例的控制台日志输出,你可以看到 Redux 状态在调度每个操作时如何变化:
¥If we look at the console log output from that example, you can see how the Redux state changes as each action was dispatched:
请注意,我们的应用没有记录上次操作的任何内容。这是因为我们在调用 unsubscribe()
时删除了监听器回调,因此在分派操作后不会运行任何其他操作。
¥Notice that our app did not log anything from the last action. That's because we removed the listener callback when we called unsubscribe()
, so nothing else ran after the action was dispatched.
我们在开始编写 UI 之前就指定了应用的行为。这有助于让我们相信该应用将按预期运行。
¥We specified the behavior of our app before we even started writing the UI. That helps give us confidence that the app will work as intended.
如果你愿意,你现在可以尝试为你的 reducer 编写测试。因为它们是 纯函数,所以测试它们应该很简单。使用示例 state
和 action
调用它们,获取结果,然后检查它是否符合你的预期:
¥If you want, you can now try writing tests for your reducers. Because they're pure functions, it should be straightforward to test them. Call them with an example state
and action
,
take the result, and check to see if it matches what you expect:
import todosReducer from './todosSlice'
test('Toggles a todo based on id', () => {
const initialState = [{ id: 0, text: 'Test text', completed: false }]
const action = { type: 'todos/todoToggled', payload: 0 }
const result = todosReducer(initialState, action)
expect(result[0].completed).toBe(true)
})
Redux Store 内部
¥Inside a Redux Store
查看 Redux 存储内部以了解其工作原理可能会有所帮助。这是一个正在运行的 Redux 存储的微型示例,大约有 25 行代码:
¥It might be helpful to take a peek inside a Redux store to see how it works. Here's a miniature example of a working Redux store, in about 25 lines of code:
function createStore(reducer, preloadedState) {
let state = preloadedState
const listeners = []
function getState() {
return state
}
function subscribe(listener) {
listeners.push(listener)
return function unsubscribe() {
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
function dispatch(action) {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
dispatch({ type: '@@redux/INIT' })
return { dispatch, subscribe, getState }
}
这个小版本的 Redux 存储运行良好,你可以使用它来替换你迄今为止在应用中使用的实际 Redux createStore
函数。(尝试一下,亲自看看!)实际的 Redux 存储实现更长并且更复杂,但其中大部分是评论、警告消息和处理一些边缘情况。
¥This small version of a Redux store works well enough that you could use it to replace the actual Redux createStore
function you've been using in your app so far. (Try it and see for yourself!) The actual Redux store implementation is longer and a bit more complicated, but most of that is comments, warning messages, and handling some edge cases.
正如你所看到的,这里的实际逻辑相当短:
¥As you can see, the actual logic here is fairly short:
存储内部有当前的
state
值和reducer
功能¥The store has the current
state
value andreducer
function inside of itselfgetState
返回当前状态值¥
getState
returns the current state valuesubscribe
保留监听器回调数组并返回一个函数以删除新回调¥
subscribe
keeps an array of listener callbacks and returns a function to remove the new callbackdispatch
调用 reducer,保存状态,并运行监听器¥
dispatch
calls the reducer, saves the state, and runs the listeners存储在启动时分派一个操作,以使用 reducer 的状态来初始化它们
¥The store dispatches one action on startup to initialize the reducers with their state
store API 是一个内部有
{dispatch, subscribe, getState}
的对象¥The store API is an object with
{dispatch, subscribe, getState}
inside
特别强调其中之一:请注意,getState
仅返回当前 state
值。这意味着默认情况下,没有什么可以阻止你意外改变当前状态值!这段代码运行时不会出现任何错误,但它是不正确的:
¥To emphasize one of those in particular: notice that getState
just returns whatever the current state
value is. That means that by default, nothing prevents you from accidentally mutating the current state value! This code will run without any errors, but it's incorrect:
const state = store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status = 'Active'
换句话说:
¥In other words:
当你调用
getState()
时,Redux 存储不会生成state
值的额外副本。它与从根 reducer 函数返回的引用完全相同¥The Redux store doesn't make an extra copy of the
state
value when you callgetState()
. It's exactly the same reference that was returned from the root reducer functionRedux 存储不会做任何其他事情来防止意外突变。无论是在 reducer 内部还是在存储之外,都有可能改变状态,并且你必须始终小心避免突变。
¥The Redux store doesn't do anything else to prevent accidental mutations. It is possible to mutate the state, either inside a reducer or outside the store, and you must always be careful to avoid mutations.
意外突变的常见原因之一是对数组进行排序。调用 array.sort()
实际上会改变现有数组。如果我们调用 const sortedTodos = state.todos.sort()
,我们最终会无意中改变真实的存储状态。
¥One common cause of accidental mutations is sorting arrays. Calling array.sort()
actually mutates the existing array. If we called const sortedTodos = state.todos.sort()
, we'd end up mutating the real store state unintentionally.
在 第 8 部分:现代回归 中,我们将看到 Redux Toolkit 如何帮助避免 reducers 中的突变,以及检测和警告 reducers 外部的意外突变。
¥In Part 8: Modern Redux, we'll see how Redux Toolkit helps avoid mutations in reducers, and detects and warns about accidental mutations outside of reducers.
配置存储
¥Configuring the Store
我们已经看到可以将 rootReducer
和 preloadedState
参数传递给 createStore
。然而,createStore
还可以多一个参数,用于定制存储的能力并赋予它新的权力。
¥We've already seen that we can pass rootReducer
and preloadedState
arguments to createStore
. However, createStore
can also take one more argument, which is used to customize the store's abilities and give it new powers.
Redux 存储是使用存储增强器来定制的。存储增强器就像 createStore
的特殊版本,它在原始 Redux 存储周围添加了另一层。然后,增强的存储可以通过提供存储的 dispatch
、getState
和 subscribe
功能的自己版本而不是原始版本来更改存储的行为方式。
¥Redux stores are customized using something called a store enhancer. A store enhancer is like a special version of createStore
that adds another layer wrapping around the original Redux store. An enhanced store can then change how the store behaves, by supplying its own versions of the store's dispatch
, getState
, and subscribe
functions instead of the originals.
在本教程中,我们不会详细介绍存储增强器的实际工作原理 - 我们将重点讨论如何使用它们。
¥For this tutorial, we won't go into details about how store enhancers actually work - we'll focus on how to use them.
使用增强器创建存储
¥Creating a Store with Enhancers
我们的项目有两个可用的小示例存储增强器,位于 src/exampleAddons/enhancers.js
文件中:
¥Our project has two small example store enhancers available, in the src/exampleAddons/enhancers.js
file:
sayHiOnDispatch
:每次调度操作时始终将'Hi'!
记录到控制台的增强器¥
sayHiOnDispatch
: an enhancer that always logs'Hi'!
to the console every time an action is dispatchedincludeMeaningOfLife
:始终将字段meaningOfLife: 42
添加到从getState()
返回的值的增强器¥
includeMeaningOfLife
: an enhancer that always adds the fieldmeaningOfLife: 42
to the value returned fromgetState()
让我们从使用 sayHiOnDispatch
开始。首先,我们将导入它,并将其传递给 createStore
:
¥Let's start by using sayHiOnDispatch
. First, we'll import it, and pass it to createStore
:
import { createStore } from 'redux'
import rootReducer from './reducer'
import { sayHiOnDispatch } from './exampleAddons/enhancers'
const store = createStore(rootReducer, undefined, sayHiOnDispatch)
export default store
我们这里没有 preloadedState
值,因此我们将传递 undefined
作为第二个参数。
¥We don't have a preloadedState
value here, so we'll pass undefined
as the second argument instead.
接下来,让我们尝试调度一个动作:
¥Next, let's try dispatching an action:
import store from './store'
console.log('Dispatching action')
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
console.log('Dispatch complete')
现在看看控制台。你应该在其他两个日志语句之间看到 'Hi!'
记录在那里:
¥Now look at the console. You should see 'Hi!'
logged there, in between the other two log statements:
sayHiOnDispatch
增强器用它自己的专门版本的 dispatch
封装了原始的 store.dispatch
功能。当我们调用 store.dispatch()
时,我们实际上是从 sayHiOnDispatch
调用封装函数,该函数调用原始函数,然后打印 '你好'。
¥The sayHiOnDispatch
enhancer wrapped the original store.dispatch
function with its own specialized version of dispatch
. When we called store.dispatch()
, we were actually calling the wrapper function from sayHiOnDispatch
, which called the original and then printed 'Hi'.
现在,让我们尝试添加第二个增强剂。我们可以从同一个文件导入 includeMeaningOfLife
...但我们有一个问题。createStore
只接受一个增强子作为其第三个参数!怎样才能同时传递两个增强子呢?
¥Now, let's try adding a second enhancer. We can import includeMeaningOfLife
from that same file... but we have a problem. createStore
only accepts one enhancer as its third argument! How can we pass two enhancers at the same time?
我们真正需要的是某种方法将 sayHiOnDispatch
增强器和 includeMeaningOfLife
增强器合并为单个组合增强器,然后将其传递。
¥What we really need is some way to merge both the sayHiOnDispatch
enhancer and the includeMeaningOfLife
enhancer into a single combined enhancer, and then pass that instead.
幸运的是,Redux 核心包含 compose
函数,可用于将多个增强器合并在一起。让我们在这里使用它:
¥Fortunately, the Redux core includes a compose
function that can be used to merge multiple enhancers together. Let's use that here:
import { createStore, compose } from 'redux'
import rootReducer from './reducer'
import {
sayHiOnDispatch,
includeMeaningOfLife
} from './exampleAddons/enhancers'
const composedEnhancer = compose(sayHiOnDispatch, includeMeaningOfLife)
const store = createStore(rootReducer, undefined, composedEnhancer)
export default store
现在我们可以看看如果我们使用 store 会发生什么:
¥Now we can see what happens if we use the store:
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: 'Hi!'
console.log('State after dispatch: ', store.getState())
// log: {todos: [...], filters: {status, colors}, meaningOfLife: 42}
记录的输出如下所示:
¥And the logged output looks like this:
因此,我们可以看到两个增强器同时修改存储的行为。sayHiOnDispatch
改变了 dispatch
的工作方式,includeMeaningOfLife
改变了 getState
的工作方式。
¥So, we can see that both enhancers are modifying the behavior of the store at the same time. sayHiOnDispatch
has changed how dispatch
works, and includeMeaningOfLife
has changed how getState
works.
存储增强器是修改存储的一种非常强大的方式,几乎所有 Redux 应用在设置存储时都会至少包含一个增强器。
¥Store enhancers are a very powerful way to modify the store, and almost all Redux apps will include at least one enhancer when setting up the store.
如果你没有任何 preloadedState
要传入,则可以将 enhancer
作为第二个参数传递:
¥If you don't have any preloadedState
to pass in, you can pass the enhancer
as the second argument instead:
const store = createStore(rootReducer, storeEnhancer)
中间件
¥Middleware
增强器非常强大,因为它们可以覆盖或替换任何存储的方法:dispatch
、getState
和 subscribe
。
¥Enhancers are powerful because they can override or replace any of the store's methods: dispatch
, getState
, and subscribe
.
但是,很多时候,我们只需要自定义 dispatch
的行为方式即可。如果有一种方法可以在 dispatch
运行时添加一些自定义行为,那就太好了。
¥But, much of the time, we only need to customize how dispatch
behaves. It would be nice if there was a way to add some customized behavior when dispatch
runs.
Redux 使用一种称为中间件的特殊插件来让我们自定义 dispatch
功能。
¥Redux uses a special kind of addon called middleware to let us customize the dispatch
function.
如果你曾经使用过 Express 或 Koa 等库,你可能已经熟悉添加中间件来自定义行为的想法。在这些框架中,中间件是你可以在接收请求的框架和生成响应的框架之间放置的一些代码。例如,Express 或 Koa 中间件可能会添加 CORS 标头、日志记录、压缩等。中间件的最大特点是它可以在链中组合。你可以在单个项目中使用多个独立的第三方中间件。
¥If you've ever used a library like Express or Koa, you might already be familiar with the idea of adding middleware to customize behavior. In these frameworks, middleware is some code you can put between the framework receiving a request, and the framework generating a response. For example, Express or Koa middleware may add CORS headers, logging, compression, and more. The best feature of middleware is that it's composable in a chain. You can use multiple independent third-party middleware in a single project.
Redux 中间件解决的问题与 Express 或 Koa 中间件不同,但概念上类似。Redux 中间件在分派操作和到达 reducer 之间提供了第三方扩展点。人们使用 Redux 中间件进行日志记录、崩溃报告、与异步 API 交互、路由等。
¥Redux middleware solves different problems than Express or Koa middleware, but in a conceptually similar way. Redux middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. People use Redux middleware for logging, crash reporting, talking to an asynchronous API, routing, and more.
首先,我们将了解如何将中间件添加到存储,然后我们将向你展示如何编写自己的中间件。
¥First, we'll look at how to add middleware to the store, then we'll show how you can write your own.
使用中间件
¥Using Middleware
我们已经看到你可以使用存储增强器自定义 Redux 存储。Redux 中间件实际上是在 Redux 内置的一个非常特殊的存储增强器(称为 applyMiddleware
)之上实现的。
¥We already saw that you can customize a Redux store using store enhancers. Redux middleware are actually implemented on top of a very special store enhancer that comes built in with Redux, called applyMiddleware
.
由于我们已经知道如何向我们的存储添加增强器,因此我们现在应该能够做到这一点。我们将从 applyMiddleware
本身开始,然后添加该项目中包含的三个示例中间件。
¥Since we already know how to add enhancers to our store, we should be able to do that now. We'll start with applyMiddleware
by itself, and we'll add three example middleware that have been included in this project.
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
export default store
正如其名称所示,每个中间件都会在分派操作时打印一个数字。
¥As their names say, each of these middleware will print a number when an action is dispatched.
如果我们现在发货会怎样?
¥What happens if we dispatch now?
import store from './store'
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
// log: '1'
// log: '2'
// log: '3'
我们可以在控制台中看到输出:
¥And we can see the output in the console:
那么它是如何运作的呢?
¥So how does that work?
中间件围绕存储的 dispatch
方法形成管道。当我们调用 store.dispatch(action)
时,我们实际上是在调用管道中的第一个中间件。当该中间件看到该操作时,它就可以做任何它想做的事情。通常,中间件会检查该操作是否是它所关心的特定类型,就像 reducer 一样。如果类型正确,中间件可能会运行一些自定义逻辑。否则,它将操作传递给管道中的下一个中间件。
¥Middleware form a pipeline around the store's dispatch
method. When we call store.dispatch(action)
, we're actually calling the first middleware in the pipeline. That middleware can then do anything it wants when it sees the action. Typically, a middleware will check to see if the action is a specific type that it cares about, much like a reducer would. If it's the right type, the middleware might run some custom logic. Otherwise, it passes the action to the next middleware in the pipeline.
与 reducer 不同,中间件内部可能有副作用,包括超时和其他异步逻辑。
¥Unlike a reducer, middleware can have side effects inside, including timeouts and other async logic.
在这种情况下,操作将通过:
¥In this case, the action is passed through:
print1
中间件(我们将其视为store.dispatch
)¥The
print1
middleware (which we see asstore.dispatch
)print2
中间件¥The
print2
middlewareprint3
中间件¥The
print3
middleware原来的
store.dispatch
¥The original
store.dispatch
store
内根部 reducer¥The root reducer inside
store
由于这些都是函数调用,因此它们都从该调用堆栈返回。因此,print1
中间件是第一个运行的,也是最后一个完成的。
¥And since these are all function calls, they all return from that call stack. So, the print1
middleware is the first to run, and the last to finish.
编写自定义中间件
¥Writing Custom Middleware
我们也可以编写自己的中间件。你可能不需要一直这样做,但自定义中间件是将特定行为添加到 Redux 应用的好方法。
¥We can also write our own middleware. You might not need to do this all the time, but custom middleware are a great way to add specific behaviors to a Redux application.
Redux 中间件被编写为一系列三个嵌套函数。让我们看看这个模式是什么样子的。我们将首先尝试使用 function
关键字编写这个中间件,以便更清楚发生了什么:
¥Redux middleware are written as a series of three nested functions. Let's see what that pattern looks like. We'll start by trying to write this middleware using the function
keyword, so that it's more clear what's happening:
// Middleware written as ES5 functions
// Outer function:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here
return next(action)
}
}
}
让我们分解一下这三个函数的作用以及它们的参数是什么。
¥Let's break down what these three functions do and what their arguments are.
exampleMiddleware
:外部函数实际上是 "中间件" 本身。它将由applyMiddleware
调用,并接收包含存储的{dispatch, getState}
函数的storeAPI
对象。这些功能与dispatch
和getState
功能相同,实际上是存储的一部分。如果你调用此dispatch
函数,它将将该操作发送到中间件管道的开头。这仅被调用一次。¥
exampleMiddleware
: The outer function is actually the "middleware" itself. It will be called byapplyMiddleware
, and receives astoreAPI
object containing the store's{dispatch, getState}
functions. These are the samedispatch
andgetState
functions that are actually part of the store. If you call thisdispatch
function, it will send the action to the start of the middleware pipeline. This is only called once.wrapDispatch
:中间函数接收一个名为next
的函数作为其参数。该函数实际上是管道中的下一个中间件。如果这个中间件是序列中的最后一个,那么next
实际上是原始的store.dispatch
函数。调用next(action)
会将操作传递给管道中的下一个中间件。这也只被调用一次¥
wrapDispatch
: The middle function receives a function callednext
as its argument. This function is actually the next middleware in the pipeline. If this middleware is the last one in the sequence, thennext
is actually the originalstore.dispatch
function instead. Callingnext(action)
passes the action to the next middleware in the pipeline. This is also only called oncehandleAction
:最后,内部函数接收当前的action
作为其参数,并且每次调度动作时都会调用该函数。¥
handleAction
: Finally, the inner function receives the currentaction
as its argument, and will be called every time an action is dispatched.
你可以为这些中间件函数指定任何你想要的名称,但使用这些名称有助于记住每个中间件函数的作用:
¥You can give these middleware functions any names you want, but it can help to use these names to remember what each one does:
外:
someCustomMiddleware
(或者无论你的中间件叫什么)¥Outer:
someCustomMiddleware
(or whatever your middleware is called)中间:
wrapDispatch
¥Middle:
wrapDispatch
内:
handleAction
¥Inner:
handleAction
因为这些都是普通函数,所以我们也可以使用 ES2015 箭头函数来编写它们。这让我们可以把它们写得更短,因为箭头函数不必有 return
语句,但如果你还不熟悉箭头函数和隐式返回,它也可能会有点难以阅读。
¥Because these are normal functions, we can also write them using ES2015 arrow functions. This lets us write them shorter because arrow functions don't have to have a return
statement, but it can also be a bit harder to read if you're not yet familiar with arrow functions and implicit returns.
这是与上面相同的示例,使用箭头函数:
¥Here's the same example as above, using arrow functions:
const anotherExampleMiddleware = storeAPI => next => action => {
// Do something in here, when each action is dispatched
return next(action)
}
我们仍然将这三个函数嵌套在一起,并返回每个函数,但隐式返回使这个时间更短。
¥We're still nesting those three functions together, and returning each function, but the implicit returns make this shorter.
你的第一个自定义中间件
¥Your First Custom Middleware
假设我们想向应用添加一些日志记录。We'我希望在调度每个操作时在控制台中查看其内容,并且我们'd 希望了解 reducer 处理操作后的状态。
¥Let's say we want to add some logging to our application. We'd like to see the contents of each action in the console when it's dispatched, and we'd like to see what the state is after the action has been handled by the reducers.
这些示例中间件并不是实际待办事项应用的特定部分,但你可以尝试将它们添加到你的项目中,看看使用它们时会发生什么。
¥These example middleware aren't specifically part of the actual todo app, but you can try adding them to your project to see what happens when you use them.
我们可以编写一个小型中间件,将这些信息记录到控制台:
¥We can write a small middleware that will log that information to the console for us:
const loggerMiddleware = storeAPI => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', storeAPI.getState())
return result
}
每当调度一个动作时:
¥Whenever an action is dispatched:
handleAction
函数的第一部分运行,我们打印'dispatching'
¥The first part of the
handleAction
function runs, and we print'dispatching'
我们将操作传递给
next
部分,该部分可能是另一个中间件或真正的store.dispatch
¥We pass the action to the
next
section, which may be another middleware or the realstore.dispatch
最终,reducer 运行并更新状态,
next
函数返回¥Eventually the reducers run and the state is updated, and the
next
function returns我们现在可以调用
storeAPI.getState()
并查看新状态是什么¥We can now call
storeAPI.getState()
and see what the new state is最后,我们返回来自
next
中间件的result
值¥We finish by returning whatever
result
value came from thenext
middleware
任何中间件都可以返回任何值,管道中第一个中间件的返回值实际上是在调用 store.dispatch()
时返回的。例如:
¥Any middleware can return any value, and the return value from the first middleware in the pipeline is actually returned when you call store.dispatch()
. For example:
const alwaysReturnHelloMiddleware = storeAPI => next => action => {
const originalResult = next(action)
// Ignore the original result, return something else
return 'Hello!'
}
const middlewareEnhancer = applyMiddleware(alwaysReturnHelloMiddleware)
const store = createStore(rootReducer, middlewareEnhancer)
const dispatchResult = store.dispatch({ type: 'some/action' })
console.log(dispatchResult)
// log: 'Hello!'
让我们再尝试一个例子。中间件通常会查找特定的操作,然后在分派该操作时执行某些操作。中间件还具有在内部运行异步逻辑的能力。我们可以编写一个中间件,当它看到某个动作时,它会延迟打印一些内容:
¥Let's try one more example. Middleware often look for a specific action, and then do something when that action is dispatched. Middleware also have the ability to run async logic inside. We can write a middleware that prints something on a delay when it sees a certain action:
const delayedMessageMiddleware = storeAPI => next => action => {
if (action.type === 'todos/todoAdded') {
setTimeout(() => {
console.log('Added a new todo: ', action.payload)
}, 1000)
}
return next(action)
}
该中间件将查找 "添加了待办事项" 操作。每次它看到一个,它就会设置一个 1 秒计时器,然后将操作的有效负载打印到控制台。
¥This middleware will look for "todo added" actions. Every time it sees one, it sets a 1-second timer, and then prints the action's payload to the console.
中间件用例
¥Middleware Use Cases
那么,我们可以用中间件做什么呢?很多东西!
¥So, what can we do with middleware? Lots of things!
当中间件看到分派的操作时,它可以做任何它想做的事情:
¥A middleware can do anything it wants when it sees a dispatched action:
将一些内容记录到控制台
¥Log something to the console
设置超时
¥Set timeouts
进行异步 API 调用
¥Make asynchronous API calls
修改动作
¥Modify the action
暂停动作甚至完全停止
¥Pause the action or even stop it entirely
以及你能想到的任何其他内容。
¥and anything else you can think of.
特别是,中间件旨在包含具有副作用的逻辑。此外,中间件可以修改 dispatch
以接受非普通操作对象的事物。我们将详细讨论这两个 在第 6 部分中:异步逻辑。
¥In particular, middleware are intended to contain logic with side effects. In addition, middleware can modify dispatch
to accept things that are not plain action objects. We'll talk more about both of these in Part 6: Async Logic.
Redux 开发工具
¥Redux DevTools
最后,配置存储时还有一件非常重要的事情需要讨论。
¥Finally, there's one more very important thing to cover with configuring the store.
Redux 经过专门设计,可以让你更轻松地了解状态随时间发生变化的时间、地点、原因和方式。作为其中的一部分,Redux 的构建是为了支持使用 Redux DevTools - 一个插件,可显示已调度的操作、这些操作包含的内容以及每个已调度的操作后状态如何变化的历史记录。
¥Redux was specifically designed to make it easier to understand when, where, why, and how your state has changed over time. As part of that, Redux was built to enable the use of the Redux DevTools - an addon that shows you a history of what actions were dispatched, what those actions contained, and how the state changed after each dispatched action.
Redux DevTools UI 可作为 Chrome 和 火狐浏览器 的浏览器扩展。如果你尚未将其添加到浏览器中,请立即执行此操作。
¥The Redux DevTools UI is available as a browser extension for Chrome and Firefox. If you haven't already added that to your browser, go ahead and do that now.
安装完成后,打开浏览器的 DevTools 窗口。你现在应该会看到一个新的 "Redux" 选项卡。它还没有做任何事情 - 我们必须先将其设置为与 Redux 存储对话。
¥Once that's installed, open up the browser's DevTools window. You should now see a new "Redux" tab there. It doesn't do anything, yet - we've got to set it up to talk to a Redux store first.
将 DevTools 添加到存储
¥Adding the DevTools to the Store
安装扩展后,我们需要配置存储,以便 DevTools 可以看到内部发生的情况。DevTools 需要添加特定的存储增强器才能实现这一点。
¥Once the extension is installed, we need to configure the store so that the DevTools can see what's happening inside. The DevTools require a specific store enhancer to be added to make that possible.
Redux DevTools 扩展文档 有一些关于如何设置存储的说明,但列出的步骤有点复杂。然而,有一个名为 redux-devtools-extension
的 NPM 包可以处理复杂的部分。该包导出了一个专门的 composeWithDevTools
函数,我们可以使用它来代替原始的 Redux compose
函数。
¥The Redux DevTools Extension docs have some instructions on how to set up the store, but the steps listed are a bit complicated. However, there's an NPM package called redux-devtools-extension
that takes care of the complicated part. That package exports a specialized composeWithDevTools
function that we can use instead of the original Redux compose
function.
看起来是这样的:
¥Here's how that looks:
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const composedEnhancer = composeWithDevTools(
// EXAMPLE: Add whatever middleware you actually want to use here
applyMiddleware(print1, print2, print3)
// other store enhancers if any
)
const store = createStore(rootReducer, composedEnhancer)
export default store
确保导入存储后 index.js
仍在调度操作。现在,在浏览器的 DevTools 窗口中打开 Redux DevTools 选项卡。你应该看到如下所示的内容:
¥Make sure that index.js
is still dispatching an action after importing the store. Now, open up the Redux DevTools tab in the browser's DevTools window. You should see something that looks like this:
左侧有一个已调度操作的列表。如果我们单击其中之一,右侧窗格会显示几个选项卡:
¥There's a list of dispatched actions on the left. If we click one of them, the right pane shows several tabs:
该操作对象的内容
¥The contents of that action object
reducer 运行后的整个 Redux 状态
¥The entire Redux state as it looked after the reducer ran
前一个状态和当前状态之间的差异
¥The diff between the previous state and this state
如果启用,函数堆栈跟踪将返回到首先调用
store.dispatch()
的代码行¥If enabled, the function stack trace leading back to the line of code that called
store.dispatch()
in the first place
这是我们发送 "添加待办事项" 操作后 "状态" 和 "差异" 选项卡的样子:
¥Here's what the "State" and "Diff" tabs look like after we dispatched that "add todo" action:
这些是非常强大的工具,可以帮助我们调试应用并准确了解内部发生的情况。
¥These are very powerful tools that can help us debug our apps and understand exactly what's happening inside.
你学到了什么
¥What You've Learned
正如你所看到的,存储是每个 Redux 应用的核心部分。存储通过运行 reducer 来包含状态和处理操作,并且可以进行定制以添加其他行为。
¥As you've seen, the store is the central piece of every Redux application. Stores contain state and handle actions by running reducers, and can be customized to add additional behaviors.
让我们看看我们的示例应用现在的样子:
¥Let's see how our example app looks now:
提醒一下,这是我们在本节中介绍的内容:
¥And as a reminder, here's what we covered in this section:
Redux 应用始终有一个存储
¥Redux apps always have a single store
存储是使用 Redux
createStore
API 创建的¥Stores are created with the Redux
createStore
API每个存储都有一个单根 reducer 功能
¥Every store has a single root reducer function
存储主要有三种方式
¥Stores have three main methods
getState
返回当前状态¥
getState
returns the current statedispatch
向 reducer 发送一个 action 来更新状态¥
dispatch
sends an action to the reducer to update the statesubscribe
采用每次分派操作时运行的监听器回调¥
subscribe
takes a listener callback that runs each time an action is dispatched
存储增强器让我们可以在创建存储时对其进行自定义
¥Store enhancers let us customize the store when it's created
增强器封装存储并可以覆盖其方法
¥Enhancers wrap the store and can override its methods
createStore
接受一个增强子作为参数¥
createStore
accepts one enhancer as an argument可以使用
compose
API 将多个增强器合并在一起¥Multiple enhancers can be merged together using the
compose
API
中间件是定制存储的主要方式
¥Middleware are the main way to customize the store
使用
applyMiddleware
增强器添加中间件¥Middleware are added using the
applyMiddleware
enhancer中间件被编写为三个相互嵌套的函数
¥Middleware are written as three nested functions inside each other
每次分派操作时都会运行中间件
¥Middleware run each time an action is dispatched
中间件内部可能会产生副作用
¥Middleware can have side effects inside
Redux DevTools 可让你查看应用随时间的变化
¥The Redux DevTools let you see what's changed in your app over time
DevTools 扩展可以安装在你的浏览器中
¥The DevTools Extension can be installed in your browser
存储需要添加 DevTools 增强器,使用
composeWithDevTools
¥The store needs the DevTools enhancer added, using
composeWithDevTools
DevTools 显示调度的操作和状态随时间的变化
¥The DevTools show dispatched actions and changes in state over time
下一步是什么?
¥What's Next?
我们现在有一个有效的 Redux 存储,可以运行我们的 reducer 并在我们分派操作时更新状态。
¥We now have a working Redux store that can run our reducers and update the state when we dispatch actions.
然而,每个应用都需要一个用户界面来显示数据并让用户做一些有用的事情。在 第 5 部分:UI 和 React 中,我们将了解 Redux 存储如何与 UI 配合使用,特别是了解 Redux 如何与 React 配合使用。
¥However, every app needs a user interface to display the data and let the user do something useful. In Part 5: UI and React, we'll see how the Redux store works with a UI, and specifically see how Redux can work together with React.