Redux 是一个用于JavaScript应用的、可预测状态容器。Redux由Flux演变而来,参考了Elm,并避开了Flux的复杂性。可以将Redux与React或任何其他视图库一起使用。它很小(2kB
,包括依赖项),但是大量可用的插件生态系统。
本文不是对Redux 官方文档的完整翻译,因本人学习Redux需要(基于Node及React使用环境),只摘录其中所需要的部分。Redux 官方提供了丰富、完整的资料\文档,非官方学习资源也非常多。Redux 相关资源:
- Redux官网
- Redux GitHub
- Redux 官方文档
- Redux 中文文档
1. 介绍
1.1 开始使用
Redux 可以帮我们编写性能一致、适用于不同环境(客户端、服务器和本机)中运行、且易于测试的应用程序。最重要的是,它提供了出色的开发体验,例如:实时代码编辑和及调试器工具。
Redux 可以与React或任何其他视图库一起使用。很小,但有大量可用的插件生态系统。
1.1.1 安装
Redux已作为NPM包提供,在Node环境中可使用npm
或yarn
安装:
# NPM npm install --save redux # Yarn yarn add redux
其同样可作为预编译UDM(通用模块定义规范,Universal Module Definition)包来使用,在浏览器环境下该包定义了window.Redux
全局变量。直接通过<script>
标签用即可。
更多详细信息,请参见官方安装文档。
1.1.2 Redux 开发工具包
Redux本身很小且不受限制。官方还提供了一个可做为插件开发工具包:Redux Toolkit(RTK),其中包括一些默认值,可以帮我们更有效地使用Redux。这是官方推荐的编写Redux逻辑的方法。
Redux Toolkit中包含了很多简单示例,包括:store
设置、创建reducer
和编写不可变更新逻辑、甚至创建状态切片。
无论刚开始创建项目的Redux新用户,还是想简化现有应用的资深用户,Redux Toolkit都可以对你有所帮助。
1.1.3 简单示例
应用的整个状态都存储在单个store
的对象树中。更改状态(state
)树的唯一方法是发出一个动作(action
),一个用于描述所发生情况的对象。要指定动作如何转换状态树,可以编写纯函数形式的reducer
。
如下所示:
import { createStore } from 'redux' /** * 这是一个 `reducer`, 其是签名形式为 `(state, action) => state` 的纯函数。 * 它描述了动作(`action`)如何将当前状态转换为下一状态 * * 状态(`state`)的形状由你决定,它可以是:原始值、数组、对象、甚至`Immutable.js`数据结构。 * 唯一重要的部分是,你不应该直接修改状态对象,而是要在状态更改时返回一个新对象。 * * 在本示例中,我们使用了`switch`语句和字符串, * 但是如果对于你的项来说,则可以遵循不同的项目约定(如函数映射)。 */ function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } } // 创建一个保存应用程序的Redux存储(`store`) // 其API为 `{ subscribe, dispatch, getState }` let store = createStore(counter) store.subscribe(()=> console.log(store.getState())) // 可以用 `subscribe()` 来更新UI以响应状态更改 // 通常,会使用视图绑定库(例如React Redux),而不是直接使用 `subscribe()` // 而且,将当前状态保存在`localStorage`中也很方便:`localStorage. store.subscribe(() => console.log(store.getState()))` // 改变内部状态的唯一方法是发送(`dispatch`)一个动作。 // 这些动作可以序列化、记录或存储,并在之后重用。 // The actions can be serialized, logged or stored and later replayed. store.dispatch({ type: 'INCREMENT' }) // 1 store.dispatch({ type: 'INCREMENT' }) // 2 store.dispatch({ type: 'DECREMENT' }) // 1
可以直接使用原生对象action
来指定要发生的变化,而不是直接修改状态。然后,编写一个称为reducer
的特殊函数,以决定每个动作如何转换整个应用的状态。
在典型的Redux应用中,只有一个存储(store
),其有唯一的根reducer
函数的。随着你的应用的增长,会将根reducer
拆分为较小的reducer
,这些reducer
分别在状态树的不同部分运行。这就像在React应用中只有一个根组件一样,但它由许多小组件组成。
对于简单应用而言,这种体系结构似乎有些过头,但是这种模式的优点在于可以很好地扩展到大型和复杂的应用中。还可以启用非常强大的开发人员工具,因为可以跟踪每个变化及引起变化的动作。可以通过记录用户会话并,仅通过重放每个操作来重现它们。
1.1.4 示例项目
Redux存储库包含了几个示例项目,这些示例展示了Redux的在各个方面的使用。几乎所有示例都有对应的CodeSandbox沙箱环境。你可以通过这些示例,在线交互式使用。
详细示例请参考官方示例文档。
1.2 动机
随着对JavaScript单页应用的需求变得越来越复杂,我们的代码必须管理比以往更多的状态(state
)。这些状态可以包括服务器响应、缓存的数据、以及本地创建尚未持久保存到服务器的数据。UI状态的复杂性也在增加,我们需要管理活动状态的路由、选中的选项卡、动效、分页控件等。
管理这种不断变化的状态非常困难。如果一个模型(Model
)的更新会导致其它模型的更新,那么视图(View
)的变化同样也会引起模型的更新,而更新的模型又更新另一个模型,模型的更新又导致另一个视图更新。有时,我们甚至不知道应用中所发生状况,因为我们无法控制其状态更新的时间、原因和方式。当系统是不透明且不确定时,很难重现错误或添加新功能。
这还不够麻烦,请考虑一下前端相关的新需求。作为开发人员,我们可以使用乐观更新、服务端渲染、路由跳转前获取数据等。这也使前端管理变得前所未有的复杂,那么我们应该放弃吗?当然不。
这种复杂性很难处理,因为我们混用了两个难以理解的概念:变化和异步。如果两者分开可能很简单,但放在一起就会变得很乱。像React这样的库试图通过在视图层消除异步和直接DOM操作,来解决的这一问题,管理数据状态(state
)仍由自己管理。这就是需要用到Redux的地方。
就像Flux、CQRS和Event Sourcing,Redux试图通过限制更新的方式和时间,使状态变化变得可预测。这些限制都体现在Redux的三大原则中。
1.3 核心概念
应用状态(state
)可以被描述为一个普通对象。例如,一个Todo应用的状态可能如下所示:
{ todos: [{ text: 'Eat food', completed: true }, { text: 'Exercise', completed: false }], visibilityFilter: 'SHOW_COMPLETED' }
这个对象就一个“模型”(Model
)
,只是其没有Setter
。这样,其它部分代码就不能随意修改,从而导致难以复现的Bug。
要修改状态就需要发送一个动作(action
)。动作是一个是普通JavaScript对象,它描述发生了什么。以下是一些示例操作:
{ type: 'ADD_TODO', text: 'Go to swimming pool' } { type: 'TOGGLE_TODO', index: 1 } { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
强制将每项修改都作为一个动作,我们就可以清楚地了解应用中正在发生的事情。如果有变化,我们也会知道为什么变化。动作就像所发生事件的面包屑。最后,为了将状态和动作联系在一起,我们会写一个称为reducer
的函数。它同样也没有什么神奇之处,只是一个将状态和动作作为参数并返回应用的下一个状态的函数。对于大型应用来说,不大可能只使用一个这样的函数,因此我们会编写较小的函数来管理状态的一部分:
function visibilityFilter(state = 'SHOW_ALL', action) { if (action.type === 'SET_VISIBILITY_FILTER') { return action.filter } else { return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return state.concat([{ text: action.text, completed: false }]) case 'TOGGLE_TODO': return state.map((todo, index) => action.index === index ? { text: todo.text, completed: !todo.completed } : todo ) default: return state } }
然后,我们再写另一个reducer
,通过调用这两个reducer
来管理完整的应用状态:
function todoApp(state = {}, action) { return { todos: todos(state.todos, action), visibilityFilter: visibilityFilter(state.visibilityFilter, action) } }
这基本就是Redux的整体思想。需要注意,我们尚未使用任何Redux API,其中有一些实用工具来简化这种模式,但主要思想是根据动作(action
)来对象来更新状态(state
),而且我们所编写的代码90%都是纯JavaScript,而没有使用Redux、 Redux API或任何其它魔法。
1.4 三大原则
Redux可以用以下三项基本原则来描述:
单一数据源
整个应用的状态(state)存储在单个对象树存储(store)中。
这使得创建通用应用变得容易,因为你可以将来自服务端的状态序列化并合并到客户端,而无需额外的编码。单个状态树还让应用调试及检查,更加容易。开发过程中,还可以将应用状态保存到本地,从而缩短开发周期。如果的所有状态都存储在单个状态树中,则某些传统上难以实现的功能(如:撤消/重做)也变得轻而易举。
console.log(store.getState()) /* Prints { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] } */
状态(State
)是只读的
更改状态的唯一方法是发出一个动作(action
),action
是一个描述发生了什么的对象。
这样就确保视图和网络请求都不会直接写入状态。相反,他们只表达了修改状态的意图。因为所有修改都是集中、并且严格按照顺序进行,所以没有任何要注意微妙的竞态条件。由于Action只是简单的对象,因此可以将它们记录、序列化、存储并在以后重用以进行调试或测试。
store.dispatch({ type: 'COMPLETE_TODO', index: 1 }) store.dispatch({ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_COMPLETED' })
用纯函数执行修改
要说明action
如何改变状态树,可以编写纯函数形式的reducer。
Reducer是纯函数,其接受上一个state
和一个action
做为参数,并返回新的state
(注意:记住要返回一个新的状态对象,而不是改变之前的状态)。你可以从单个reducer
开始,并随着应用的扩展,再将其拆分为较小的reducer
,以管理状态树的某一部分。因为reducer
只是函数,你可以控制它们的调用顺序、传递其他数据、甚至可以复用一些通用reducer
(如,分页)。
function visibilityFilter(state = 'SHOW_ALL', action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text, completed: false } ] case 'COMPLETE_TODO': return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: true }) } return todo }) default: return state } } import { combineReducers, createStore } from 'redux' const reducer = combineReducers({ visibilityFilter, todos }) const store = createStore(reducer)
2. 基础教程
不要被各种关于reducer
、middleware
(中间件)、store
的讨论所迷惑了-Redux其实非常简单。如果你有Flux使用经验,那你会感到非常熟悉。即使不熟悉Flux,也很容易!
以下是一个一步步构建简单Todo应用的示例:
2.1 Action
首先,让我们定义一些动作。
Action
是把数据从应用发送到store
的有效负载。它们是store
的唯一数据来源。可以使用store.dispatch()
将action
发送到store
。
以下是一个添加新的Todo任务的action
示例:
const ADD_TODO = 'ADD_TODO'
{ type: ADD_TODO, text: 'Build my first Redux app' }
Action是原生的JavaScript对象,Action 必须具有指示要执行的动作类型的type
属性。类型(type)通常应定义为字符串常量。随着应用规模变大,可能需要将它们移到单独的模块中。
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
注意:在单独的文件中定义动作类型常量不是必需,甚至根本不需要定义它们。对于小型项目来说,仅将字符串用于操作类型可能会更容易。但是,在大型项目中显式声明常量会有一些好处,可以减少样板代码以使代码库更加整洁。
除了type
之外,action 对象的结构就由你决定。如果有兴趣,请查看Flux 标准 Action以获取有关如何构建action的建议。
我们将添加另一种action类型,以描述用户已完成的Todo事项。我们通过index
引用指定的Todo,因为我们将它们存储在了数组中。在真实的应用中,我们会在每次创建新内容时都生成唯一的ID。
{ type: TOGGLE_TODO, index: 5 }
在每个操作(action)中应尽可能少的传递数据。例如,传递索引(index
)会比整个Todo对象更好。
最后,我们将添加另一种Action类型以更改当前可见的Todo事项。
{ type: SET_VISIBILITY_FILTER, filter: SHOW_COMPLETED }
2.1.1 Action Creator
Action Creator(Action 构造函数)-是用于创建动作(action)的函数。应注意:action
和action creator
是两个不同的术语。
在Redux中,Action创建器只需返回一个action:
function addTodo(text) { return { type: ADD_TODO, text } }
这样action creator将更加易于移植和测试。
在传统的Flux中,action creator一般会触发一个dispatch
,像这样:
function addTodoWithDispatch(text) { const action = { type: ADD_TODO, text } dispatch(action) }
在Redux中并非如此。相反,要实际启动 dispatch,需要将结果传给dispatch()
函数:
dispatch(addTodo(text)) dispatch(completeTodo(index))
另外,你可以创建一个已绑定的动作创建器,该创建器将自动dispatch:
const boundAddTodo = text => dispatch(addTodo(text)) const boundCompleteTodo = index => dispatch(completeTodo(index))
然后就可以直接调用了:
boundAddTodo(text) boundCompleteTodo(index)
可以在store
中通过store.dispatch()
的形式访问dispatch()
函数,但是更多情况下是使用诸如react-redux的connect()
之类的帮助方法来访问它。可以用bindActionCreators()
将多个Action创建器自动绑定到dispatch()
函数。
Action 创建器也可以是异步的非纯函数。可以通过高级教程-异步Action章节了解更多详细信息。
2.1.2 源码
actions.js
/* * action types */ export const ADD_TODO = 'ADD_TODO' export const TOGGLE_TODO = 'TOGGLE_TODO' export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' /* * other constants */ export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' } /* * action creators */ export function addTodo(text) { return { type: ADD_TODO, text } } export function toggleTodo(index) { return { type: TOGGLE_TODO, index } } export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter } }
2.2 Reducer
Reducer用于指定应用的状态(state
)如何响应发送到store
的动作(action
)。请注意,动作仅描述发生了什么,而没有描述应用状态如何变化。
2.2.1 设计状态(state
)结构
在Redux中,所有应用状态都存储在单个对象中。建议在编写任何代码之前,考虑一下这一对象的结构:应用状态对象的状态的最小表示是什么?
对于我们的Todo应用来说,我们想存储两种不同数据:
- 当前选中任务的过滤条件
- 完整任务列表
此外,还需要在状态树中存储其它数据以及一些UI状态。这样没问题,但是应将数据与UI状态分开。
{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true }, { text: 'Keep all state in a single tree', completed: false } ] }
注意:在更构建复杂应用时,不可避免的会有不同实体间的相互引用。建议尽可能的保持state
规范化,不要嵌套。将每个实体保存在以ID为主键存储对象中,并通过ID从其所在实体或列表中引用。可以把应用的状态视为数据库。normalizr的文档中有关于此种方式的详细描述。如,在实际应用中,将todosById: { id -> todo }
和todos: array <id>
都保存在状态内将是一个更好的方式,但是使本示例保持简单,我们并没有这样做。
2.2.2 处理 Action
现在我们已经确定了state对象的外观,然后就可以编写reducer
。reducer是一个纯函数,接受旧的state和action做为参数,并返回新的state。
(previousState, action) => nextState
之所以称为reducer,因为其会传给Array.prototype.reduce(reducer, ?initialValue)
,所以保持其纯净非常重要。永远不要在reducer中做以下操作:
- 修改传入参数
- 执行有副作用的操作,如:API调用及路由转换
- 调有非纯函数,如:
Date.now()
和Math.random()
在高级篇中将介绍如何执行有副作用的操作。现在,仅记住reducer必须是纯函数即可。只要传入的参数相同,其计算出的新状态就应该相同。没什么异外情况、没有副作用、没有API调用、没有变量修改。只是计算而已。
了解这些之后,就可以开始编写reducer
,并用它来调用前面定义的Action
。
我们将从指定初始状态开始。Redux将首次执行时,state会是undefined
。这时我们可以设置并返回应用的初始状态:
import { VisibilityFilters } from './actions' const initialState = { visibilityFilter: VisibilityFilters.SHOW_ALL, todos: [] } function todoApp(state, action) { if (typeof state === 'undefined') { return initialState } // For now, don't handle any actions // and just return the state given to us. return state }
一个技巧是,可以使用ES6的参数默认值语法。
function todoApp(state = initialState, action) { // For now, don't handle any actions // and just return the state given to us. return state }
接下来,可以处理SET_VISIBILITY_FILTER
了,所要做的只是修改状态中的visibilityFilter
。
import { SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' ... function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } }
注意:
- 不要修改
state
,而是用Object.assign()
返回一个新对象。这样也是错误Object.assign(state, { visibilityFilter: action.filter })
,因为其会修改第一个参数,必须把第一个参数设置为空对象{}
。也可以使用扩展运算符语法,{ ...state, ...newState }
。 - 默认(
default
)情况下,会返回旧的state
。在处理未知action时,一定要返回旧的state
。
关于Object.assign
:Object.assign()
是一个ES6语法,但所支持的浏览器并不多,因此需要使用polyfill、Babel或其它插件处理。
关于switch
及样板代码:switch
并不是真正的样板代码。Flux真正的样板是概念性的:更新必须要发送、必须在Dispatcher中注册Store、Store必须是对象。Redux通过使用纯函数Reducer代替事件发射器(emitter)解决了这些问题。
2.2.3 处理多个 Action
我们还有两个action需要要处理。就像我们在SET_VISIBILITY_FILTER
中的操作一样,我们将导入ADD_TODO
和TOGGLE_TODO
两个Action,然后扩展我们的reducer以处理ADD_TODO
。
import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' ... function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state } }
同样,也不是直接修改state
,而是返回新对象。新的todos
对象就是在旧todos
末尾加上新建的todo
,而这个新建的todo
也是基于action中的数据新建的。
最后,TOGGLE_TODO
的实现也很好理解:
case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) })
因为我们要更新数组中的指定的项目,但不能直接修改,所以必须创建一个新数组,其中除索引处的项目外,其他项目都相同。如果经常编写此类操作,可以使用如:immutability-helper、updeep、Immutable之类的帮助库。 请记住,不要在克隆state
之前修改它。
2.2.4 拆分 Reducer
目前为止我们代码如下,已经比较冗长了:
function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) }) default: return state } }
能不能使上在的代码更易读呢?todos
和visibleFilter
是完全独立的更新,有时state中字段是相互依赖的,这就需要更多考虑。在本例中,我们可能把更新todo折分为一个单独的函数:
function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) case TOGGLE_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) default: return state } }
注意:todos
仍接受state
参数,但state
是一个数组。现在todoApp
只需要把更新部分的state传给todos
函数,todos
函数本身会确定如何更新这部分数据。这称为reducer
组合,它是构建Redux应用的基本模式。
接下来,探讨一下如何组合reducer
。那么能不能抽象出一个reducer
来专门管理visibilityFilter
?当然可以:
首先,我们使用ES6 的对象解构去声明SHOW_ALL
:
const { SHOW_ALL } = VisibilityFilters
然后:
function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } }
现在,我们可以将主reducer重写为一个函数,该函数会调用reducer管理state的各个部分,并将它们组合为一个对象。它也不需要知道完整的初始状态。为各子reducer指定undefined
的初始状态就可以了。
function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
注意,每个reducer都在管理全局状态属于自己的部分。每个reducer的state
参数都不相同,并且对应于它所管理的状态的部分。
这已经看起来不错了!当应用更大时,我们可以将reducer拆分为单独的文件,并使它们完全独立并管理不同的数据块。
最后,Redux提供了一个名为combineReducers()
的实用工具,该工具执行与上面的todoApp
逻辑,这样就能处理掉一部分样板代码。有了它的帮助,我们可以像这样重写todoApp
:
import { combineReducers } from 'redux' const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp
现在等价于:
export default function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
你需要为它们分配不同的key,或调用不同的函数。以下两种编写组合reducer的方法是等效的:
const reducer = combineReducers({ a: doSomethingWithA, b: processB, c: c })
function reducer(state = {}, action) { return { a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action) } }
combineReducers()
所做的只是生成一个函数,该函数根据其键所选择的状态切片,来调用对应的reducer,并将结果再次组合为一个对象。这里没有魔法,与其它reducer一样,如果提供给它的所有reducer都没有改变状态,combineReducers()
就不会创建新对象。
2.2.5 源码
reducers.js
import { combineReducers } from 'redux' import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp
2.3 Store
在前面的部分中,我们定义了表示“所要发生”事情的action,以及根据这些动作要执行更新状态(state)的reducer。
Store
是把它们组合到一起的对象。其职责如下:
- 保持应用的state
- 通过getState()方法访问状态
- 通过dispatch()方法更新状态
- 通过subscribe(listener)注册状态监听器
- 通过subscribe(listener)返回函数注销状态监听器
最重要的是,在Redux应用中只能有一个store。当需要拆分数据处理逻辑时,应该使用reducer处理而不是创建多个store。
根据已有reducer创建store非常容易。在上节中,我们通过combineReducers()
将多个reducer组合到了一块。现在我们将其导入,并传给createStore()
:
import { createStore } from 'redux' import todoApp from './reducers' const store = createStore(todoApp)
createStore()
还有第二个可选参数,用于指定初始state
。
const store = createStore(todoApp, window.STATE_FROM_SERVER)
2.3.1 发送 Action
现在我们已经创建了一个store,让我们来验证一下!虽然没有任何UI,但已经可以测试更新逻辑了。
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' // Log the initial state console.log(store.getState()) // Every time the state changes, log it // Note that subscribe() returns a function for unregistering the listener const unsubscribe = store.subscribe(() => console.log(store.getState())) // Dispatch some actions store.dispatch(addTodo('Learn about actions')) store.dispatch(addTodo('Learn about reducers')) store.dispatch(addTodo('Learn about store')) store.dispatch(toggleTodo(0)) store.dispatch(toggleTodo(1)) store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) // Stop listening to state updates unsubscribe()
可以看到,虽然还没有开发UI的时候,但已经可以指定了应用的行为。而且这时也可以编写reducer和action以进行测试。不需要模拟任何东西,因为它们只是纯函数。只需要调用,并对它们的返回值进行断言即可。
2.3.1 源码
index.js
import { createStore } from 'redux' import todoApp from './reducers' const store = createStore(todoApp)
2.4 数据流(Data Flow)
Redux架构的核心是:严格的单向数据流。
这意味着应用中的所有数据都遵循相同的生命周期模式,从而使应用的逻辑更可预测且更易于理解。它还鼓励数据规范化,这样就可以避免使用以的多个独立且无法引用的重复数据源。
虽然Redux并非严格意义上的Flux,但它具有相同的设计思想。
在Redux应用中,数据生命周期遵循以下4个步骤:
Action是一个描述了发生了什么的原生对象。如:
{ type: 'LIKE_ARTICLE', articleId: 42 } { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } } { type: 'ADD_TODO', text: 'Read the Redux docs.' }
可以在任何地方调用store.dispatch(action)
,包括:组件中、XHR回调、甚至定时器中。
- 2. Redux store会调用你传给它的reducer函数
Store会把两个参数传给reducer函数:当前的状态(state)树和action
。例如,这个todo应用中,其根reducer收到的数据可能是这样的:
// The current application state (list of todos and chosen filter) let previousState = { visibleTodoFilter: 'SHOW_ALL', todos: [ { text: 'Read the docs.', complete: false } ] } // The action being performed (adding a todo) let action = { type: 'ADD_TODO', text: 'Understand the flow.' } // Your reducer returns the next application state let nextState = todoApp(previousState, action)
需要注意,reducer是纯函数。它仅计算下一个状态。计算结果应该是完全可预测的:多次使用相同的输入,调用后产生相同的输出。它不应该执行任何有副作用操作,如:API调用或路由跳转。这些应该在调度动作(dispatch action )之前发生。
- 3. 根reducer可以将多个reducer的输出组合到单个状态树中
根reducer的结构完全由你定义。Redux提供了combineReducers()
工具函数,可用于将根reducer“拆分”为单独的函数,每个函数管理状态树的一个分支。
这是combineReducers()
的工作方式,假设你有两个reducer,一个用于 todo列表,另一个用于当前选择的过滤器:
function todos(state = [], action) { // Somehow calculate it... return nextState } function visibleTodoFilter(state = 'SHOW_ALL', action) { // Somehow calculate it... return nextState } let todoApp = combineReducers({ todos, visibleTodoFilter })
当你发送action后,todoApp
返回的combineReducers
会调用两个reducers:
let nextTodos = todos(state.todos, action) let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
然后会将两个结果合并为一个状态树:
return { todos: nextTodos, visibleTodoFilter: nextVisibleTodoFilter }
combineReducers
是一个很有用的工具,当然你也可以不使用它,而使用自己的根reducer
。
- 4. Redux store保存了根reducer返回的完整状态树。
现在,这个新树将是你应用的下一个state!所有注册了store.subscribe(listener)
的监听器都将被调用;监听器可以调用store.getState()
以获取当前状态。
现在可以应用新状态,以更新UI。如果你使用React Redux这样的绑定库,这时就调用component.setState(newState)()
完成更新。
2.5 结合React使用
从一开始,我们就需要强调Redux与React没有关系。你可以使用React、Angular、Ember、jQuery或纯JavaScript编写Redux应用。
但是,最好Redux与React和Deku之类的库配置使用,因为这类库使你可以使用状态(state)函数描述UI,并且Redux会根据操作(action)发出状态更新。
我们将使用React来构建简单的Todo应用,以介绍Redux如何配合React使用:React Redux官方文档
2.5.1 安装React Redux
Redux默认并不包含React绑定库。你需要安装它:
npm install --save react-redux
如果不使用npm
,也可以从unpkg
<script>
标签的方式引入 UMD 包,那么会添加一个全局的window.ReactRedux
对象。
2.5.2 展示组件和容器组件
Redux的React绑定库使用“展示组件与容器组件分开”开发思想。这种方法可以使应用更易于理解,并可以更轻松地重用组件。以下是展示组件和容器组件之间差异的摘要(推荐阅读Dan Abramov的对展示性组件和容器组件的概念介绍):
展示组件 | 容器组件 | |
---|---|---|
目的 | 描述外观(框架、样式) | 描述工作流程(数据获取、状态更新 |
直接使用Redux | No | Yes |
数据来源 | 从props读取 | 监听 Redux state |
数据修改 | 调用props回调函数 | 发送 Redux action |
写入数据 | 手动 | 通常由React Redux生成 |
我们编写的大多数组件都是展示性的,但也需要一些容器组件以将它们与Redux Store连接。但这并不意味着容器组件必须位于组件树的最根部。如果容器组件变得太复杂(即它具有大量嵌套的展示性组件,并且传递了无数回调),请按照FAQ中的相关说明在组件树中引入另一个容器。
从技术上讲,可以直接使用store.subscribe()
手动编写容器组件。便并不建议您这样做,因为React Redux进行了许多难以手动完成的性能优化。因此,我们将使用React Redux提供的connect()
函数而不是编写容器组件,如下所示。
2.5.3 设计组件层次结构
在前面介绍了根状态(state)结构设计,现在我们要开始设计与之匹配的UI层次结构了。这不是仅对于Redux的实现,在React 设计思想中有对此很好的解释。
我们的设计概要很简单:需要显示一个todo清单;点击后,相关事项将被删除;显示一个新增字段,用户可以添加新事项;在页脚中,要显示一个切换,可以进行已完成/活动的todo之间的切换。
展示组件设计
以下是一个展示组件及其prop的介绍:
TodoList
用于显示 todo 列表todos: Array
以{ id, text, completed }
形式显示的 todo 项数组onTodoClick(id: number)
点击 todo 时要调用的回调函数
Todo
单个 todo 项目text: string
要显示的文本completed: boolean
是否显示删除线onClick()
todo 项点击时要调用的回调函数
Link
带有回调的链接onClick()
点击链接时要调用的回调函数
Footer
用户切换 todo 项目的地方App
根组件,会渲染所有子组件
展示组件描述外观,但并不知道数据来自何处或如何更改。只是渲染传入的东西。如果从Redux迁移到其他框架,这些组件可以完全不修改而直接使用。它们并不依赖Redux。
容器组件设计
我们需要一些容器组件以将展示组件连接到Redux。如,展示性的TodoList
组件需要一个像VisibleTodoList
这样的容器,该容器监听Redux store并根据可见性过滤器处理应用显示情况。要修改可见性过滤器,我们将提供一个FilterLink
容器组件,该组件会渲染一个Link
,该Link
会在点击时发送(dispatch)适当的操作(action):
VisibleTodoList
根据当前显示状态过滤 todo 列表 ,并渲染TodoList
FilterLink
获取当前过滤器,并渲染Link
filter: string
可见性过滤器
其它组件设计
有时很难确定某个组件是展示组件还是容器。例如,有时表单和函数实际上是结合在一起的。如下面这个小组件中:
VisibleTodoList
带有Add
按钮的输入框
2.5.4 组件实现
让我们开始编写组件代码!我们将从展示性组件开始,因此不需要考虑绑定到Redux。
展示组件实现
这些都是普通的React组件,因此我们不再详细解释。我们将编写无状态的功能性组件,除非需要使用本地状态(state)或生命周期方法。这并不意味着展示性组件必须是函数,但以这种方式实现更容易。如果需要添加本地状态、生命周期方法或性能优化,则可以将它们转换为类(Class)。
components/Todo.js
import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default Todo
components/TodoList.js
import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map((todo, index) => ( <Todo key={index} {...todo} onClick={() => onTodoClick(index)} /> ))} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired, onTodoClick: PropTypes.func.isRequired } export default TodoList
components/Link.js
import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick }) => { if (active) { return &lgt;span>{children}&lgt;/span> } return ( &lgt;a href="" onClick={e => { e.preventDefault() onClick() }} > {children} &lgt;/a> ) } Link.propTypes = { active: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func.isRequired } export default Link
components/Footer.js
import React from 'react' import FilterLink from '../containers/FilterLink' import { VisibilityFilters } from '../actions' const Footer = () => ( <p> Show: <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink> </p> ) export default Footer
容器组件实现
现在可以通过创建一些容器组件,以将这些展示性组件连接到Redux。从技术角度上讲,容器组件只是一个React组件,它通过监听store.subscribe()
来读取Redux状态树的一部分并通过属性(prop)传递给它渲染的的展示组件。可以手动编写容器组件,但更建议使用React Redux库的connect()
函数生成容器组件,该函数提供了很多有用的优化以避免不必要的重新渲染。(这样做,就可以不必自己实现React性能建议中的shouldComponentUpdate
。)
要使用connect()
,你需要定义一个名为mapStateToProps
的特殊函数,该函数描述如何将当前Redux store状态转换为要传递给要包装的展示组件的prop。例如,VisibleTodoList
需要计算要传递给TodoList
的todos
,因此我们定义了一个函数,该函数根据state.visibilityFilter
过滤state.todos
并在其mapStateToProps
中使用它:
const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) case 'SHOW_ALL': default: return todos } } const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } }
除了读取state,容器组件还可以分发动作(dispatch action)。类似的,可以定义一个名为mapDispatchToProps()
的函数,该函数接收dispatch()
方法并返回要注入到展示组件中的回调prop 。例如,我们希望VisibleTodoList
将名为onTodoClick
的prop注入TodoList
组件,并且我们希望onTodoClick
分发TOGGLE_TODO
操作:
const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } } }
最后,通过调用connect()
创建VisibleTodoList
并传入两个函数:
import { connect } from 'react-redux' const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
这些都是基础React Redux API,但是有一些技巧和强大的配置选项,你可以参考详细文档。如果你担心mapStateToProps
过于频繁地创建新对象,则可能需要了解如何通过reselect来计算派生数据。
以下是其余容器组件的实现:
containers/FilterLink.js
import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => { return { active: ownProps.filter === state.visibilityFilter } } const mapDispatchToProps = (dispatch, ownProps) => { return { onClick: () => { dispatch(setVisibilityFilter(ownProps.filter)) } } } const FilterLink = connect( mapStateToProps, mapDispatchToProps )(Link) export default FilterLink
containers/VisibleTodoList.js
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = state => { return { todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { onTodoClick: id => { dispatch(toggleTodo(id)) } } } const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
其它组件实现
containers/AddTodo.js
如前所述,AddTodo
组件的展示和逻辑都混合在一个定义中。
import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' let AddTodo = ({ dispatch }) => { let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => { input = node }} /> <button type="submit">Add Todo</button> </form> </div> ) } AddTodo = connect()(AddTodo) export default AddTodo
关于ref
属性, 可以通过这篇文档来了解。
将容器放在一个组件内
components/App.js
import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App
2.5.5 传入Store
所有容器组件都需要访问Redux store,可以对其进行监听。也可以将其作为prop传递每个容器组件。但是,这很很麻烦,因为必须使用store
对展示组件进行包装,因为其恰好在组件树中渲染了一个容器组件。
建议的使用一个名为<Provider>
的特殊React Redux组件,它可以神奇地让所有容器组件访问store,而无需显式传入。只需在渲染根组件时使用一次:
index.js
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' const store = createStore(todoApp) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
2.6 示例:Todo List
以下是我们本基础教程中构建的todo应用的完整源码。也可以在官方示例存储库查看源码,还可以通过CodeSandbox在浏览器中运行。
程序入口
index.js
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import { createStore } from 'redux' import rootReducer from './reducers' import App from './components/App' const store = createStore(rootReducer) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
创建 Action
actions/index.js
let nextTodoId = 0 export const addTodo = text => ({ type: 'ADD_TODO', id: nextTodoId++, text }) export const setVisibilityFilter = filter => ({ type: 'SET_VISIBILITY_FILTER', filter }) export const toggleTodo = id => ({ type: 'TOGGLE_TODO', id }) export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' }
Reducer
reducers/todos.js
const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, completed: false } ] case 'TOGGLE_TODO': return state.map(todo => todo.id === action.id ? { ...todo, completed: !todo.completed } : todo ) default: return state } } export default todos
reducers/visibilityFilter.js
import { VisibilityFilters } from '../actions' const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter default: return state } } export default visibilityFilter
reducers/index.js
import { combineReducers } from 'redux' import todos from './todos' import visibilityFilter from './visibilityFilter' export default combineReducers({ todos, visibilityFilter })
展示组件
components/Todo.js
import React from 'react' import PropTypes from 'prop-types' const Todo = ({ onClick, completed, text }) => ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none' }} > {text} </li> ) Todo.propTypes = { onClick: PropTypes.func.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired } export default Todo
components/TodoList.js
import React from 'react' import PropTypes from 'prop-types' import Todo from './Todo' const TodoList = ({ todos, toggleTodo }) => ( <ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} /> ))} </ul> ) TodoList.propTypes = { todos: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired }).isRequired ).isRequired, toggleTodo: PropTypes.func.isRequired } export default TodoList
components/Link.js
import React from 'react' import PropTypes from 'prop-types' const Link = ({ active, children, onClick }) => ( <button onClick={onClick} disabled={active} style={{ marginLeft: '4px' }} > {children} </button> ) Link.propTypes = { active: PropTypes.bool.isRequired, children: PropTypes.node.isRequired, onClick: PropTypes.func.isRequired } export default Link
components/Footer.js
import React from 'react' import FilterLink from '../containers/FilterLink' import { VisibilityFilters } from '../actions' const Footer = () => ( lt;div> lt;span>Show: lt;/span> lt;FilterLink filter={VisibilityFilters.SHOW_ALL}>Alllt;/FilterLink> lt;FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Activelt;/FilterLink> lt;FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completedlt;/FilterLink> lt;/div> ) export default Footer
components/App.js
import React from 'react' import Footer from './Footer' import AddTodo from '../containers/AddTodo' import VisibleTodoList from '../containers/VisibleTodoList' const App = () => ( <div> <AddTodo /> <VisibleTodoList /> <Footer /> </div> ) export default App
容器组件
containers/VisibleTodoList.js
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' import { VisibilityFilters } from '../actions' const getVisibleTodos = (todos, filter) => { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos case VisibilityFilters.SHOW_COMPLETED: return todos.filter(t => t.completed) case VisibilityFilters.SHOW_ACTIVE: return todos.filter(t => !t.completed) default: throw new Error('Unknown filter: ' + filter) } } const mapStateToProps = state => ({ todos: getVisibleTodos(state.todos, state.visibilityFilter) }) const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch(toggleTodo(id)) }) export default connect( mapStateToProps, mapDispatchToProps )(TodoList)
containers/FilterLink.js
import { connect } from 'react-redux' import { setVisibilityFilter } from '../actions' import Link from '../components/Link' const mapStateToProps = (state, ownProps) => ({ active: ownProps.filter === state.visibilityFilter }) const mapDispatchToProps = (dispatch, ownProps) => ({ onClick: () => dispatch(setVisibilityFilter(ownProps.filter)) }) export default connect( mapStateToProps, mapDispatchToProps )(Link)
其它组件
containers/AddTodo.js
import React from 'react' import { connect } from 'react-redux' import { addTodo } from '../actions' const AddTodo = ({ dispatch }) => { let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => (input = node)} /> <button type="submit">Add Todo</button> </form> </div> ) } export default connect()(AddTodo)
3. 高级教程
3.1 异步 Action
在基础指南中,我们构建了一个简单的todo应用。该示例是完全同步的,每次调度动作(action)时,状态都会立即更新。
在本指南中,我们将构建一个异步应用。它会使用Reddit API来显示所选子Reddit的当前标题,以演示如何适配Redux数据流。
Action
调用异步API时,需要关注两个时间点:开始调用的时刻和收到应答(或超时)的时刻。
我们通常会在这两个时刻更改应用状态。为此,需要调度由reducer同步处理的常规action。对于任何API请求,一般至少分派三种不同的action:
通知reducer该请求已开始的action。
reducer可以通过在状态(state)切换
isFetching
标志来处理此action。这样,UI就会知道应该显示过渡框了。通知reducer该请求成功完成的action。
reducer可以通过合并新数据到其管理的状态并重置
isFetching
来处理此action。而UI将隐藏过渡框,并显示获取的数据。通知reducer该请求失败的action。
reducer可以通过重置
isFetching
来处理此action。另外,有些reducer可能希望存储错误消息,以便UI可以显示相关信息。
可以在action中使用专用的status
字段:
{ type: 'FETCH_POSTS' } { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } { type: 'FETCH_POSTS', status: 'success', response: { ... } }
或者,为其定义单独的类型:
{ type: 'FETCH_POSTS_REQUEST' } { type: 'FETCH_POSTS_FAILURE', error: 'Oops' } { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
对于以上两种形式,你可以自行决定选择使用带标志的单一action、或使用多种action类型。多种类型的action犯错概率较小,但如果使用诸如redux-actions之类的帮助库来生成action creator和reducer,这都不是问题。
使用何种约定,应与你的团队商讨后决定并在整个应用中坚持。在本教程中,我们将使用单独的类型。
同步Action构造函数(Action Creator)
我们将从定义示例应用中所需的几种同步action类型和action creator开始。以下示例中,用户可以选择要显示的子reddit:
actions.js
(同步)
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT' export function selectSubreddit(subreddit) { return { type: SELECT_SUBREDDIT, subreddit } }
还可以通过“刷新”按钮进行更新:
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' export function invalidateSubreddit(subreddit) { return { type: INVALIDATE_SUBREDDIT, subreddit } }
这些action由用户控制交互。我们还会根据网络请求使用另一种方式,其后可以看到如何分发它们,但现在我们只需定义。
当需要获取子reddit的帖子时,我们将分发REQUEST_POSTS
操作:
export const REQUEST_POSTS = 'REQUEST_POSTS' function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit } }
将SELECT_SUBREDDIT
或INVALIDATE_SUBREDDIT
分开很重要。虽然它们可能会多次出现,但是随着应用变得越来越复杂,你可能希望独立于用户操作来获取一些数据(如:获取最受欢迎、或最旧数据)。你可能还想通过获取的内容来响应路由更改,因此将获取内与较早的特定UI事件结合是不明智的。
最后,当网络请求通过时,我们将分发RECEIVE_POSTS
:
export const RECEIVE_POSTS = 'RECEIVE_POSTS' function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map(child => child.data), receivedAt: Date.now() } }
这就是我们现在所需要了解的。稍后将讨论将这些action与网络请求一起分发的特定机制。
在真实应用中,你可能还希望在请求失败时调度action。在本示例中,将不会包含错误处理的实现,但是可以通过实际示例参考一些可能的方法。
设计状态(State)结构
和基础教程中一样,在实现之前,需要设计应用状态的结构。使用异步代码时,需要处理更多状态,因此我们需要仔细考虑。
对于初学者来说这部分可能会感到疑惑,因为还不能清楚地了解异步应用中需要哪些状态,及如何将其组织在单个树中。
我们将从最常见的用例开始:列表。Web应用通常会显示事物列表。例如:帖子列表或好友列表。我们需要确定应用可以显示哪些列表,并希望将它们分别存储在状态中,以对它们进行缓存,并仅在必要时才重新获取。
这是我们的“Reddit”应用状态可能的结构:
{ selectedSubreddit: 'frontend', postsBySubreddit: { frontend: { isFetching: true, didInvalidate: false, items: [] }, reactjs: { isFetching: false, didInvalidate: false, lastUpdated: 1439478405547, items: [ { id: 42, title: 'Confusion about Flux and Relay' }, { id: 500, title: 'Creating a Simple Application Using React JS and Flux Architecture' } ] } } }
以下是一些注意事项:
- 我们分别存储每个subreddit的信息,所以我们可以缓存每个subreddit。当用户第二次在他们之间切换时,更新将立即生效,除非必要,否则无需重新提取。也不必担心所有这些项目占用内存:除非你要处理成千上万的项目,并且你的用户很少关闭选项卡,否则将不需要进行任何清理。
- 对于每个项目列表,都需要存储:
isFetching
以显示过渡框,didInvalidate
以便之后可以对数据做过时处理,lastUpdated
以记录上次获取数据的时间,以及items
本身时进行。在真实的应用中,一般还需要存储分页状态,例如:fetchedPageCount
和nextPageUrl
。
关于嵌套实体
在此示例中,我们将接收的项目与分页信息一起存储。但是,如果使用互相引用的嵌套实体,或者让用户编辑项目,则此方法将无法正常工作。例如,用户想要编辑所获取的帖子,但是该帖子在状态树中的多个位置重复,实施起来会很困难。
如果有嵌套的实体,或者让用户编辑收到的实体,则应将它们存储为数据库状态。在分页信息中,只能通过它们的ID来引用。这样,就可以始终保持它们的最新状态。实际示例展示了此方法,以及用来normalizr标准化嵌套的API响应。使用这种方法,的状态可能如下所示:
{ selectedSubreddit: 'frontend', entities: { users: { 2: { id: 2, name: 'Andrew' } }, posts: { 42: { id: 42, title: 'Confusion about Flux and Relay', author: 2 }, 100: { id: 100, title: 'Creating a Simple Application Using React JS and Flux Architecture', author: 2 } } }, postsBySubreddit: { frontend: { isFetching: true, didInvalidate: false, items: [] }, reactjs: { isFetching: false, didInvalidate: false, lastUpdated: 1439478405547, items: [ 42, 100 ] } } }
在本示例中,我们没有对实体进一步规范化,但在实际应用中应有所考虑。
处理Action
在详细讨论与网络请求一起分发动作(action)时,我们将为上面定义的动作编写reducer。
关于Reducer结构
在这里,假设你已根据combineReducers()
了解了reducer的组成,正如基础指南的Splitting Reducers部分中所述,如果还未了解,建议先阅读该部分。
reducers.js
import { combineReducers } from 'redux' import { SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, REQUEST_POSTS, RECEIVE_POSTS } from '../actions' function selectedSubreddit(state = 'reactjs', action) { switch (action.type) { case SELECT_SUBREDDIT: return action.subreddit default: return state } } function posts( state = { isFetching: false, didInvalidate: false, items: [] }, action ) { switch (action.type) { case INVALIDATE_SUBREDDIT: return Object.assign({}, state, { didInvalidate: true }) case REQUEST_POSTS: return Object.assign({}, state, { isFetching: true, didInvalidate: false }) case RECEIVE_POSTS: return Object.assign({}, state, { isFetching: false, didInvalidate: false, items: action.posts, lastUpdated: action.receivedAt }) default: return state } } function postsBySubreddit(state = {}, action) { switch (action.type) { case INVALIDATE_SUBREDDIT: case RECEIVE_POSTS: case REQUEST_POSTS: return Object.assign({}, state, { [action.subreddit]: posts(state[action.subreddit], action) }) default: return state } } const rootReducer = combineReducers({ postsBySubreddit, selectedSubreddit }) export default rootReducer
在以上代码中,有两个有趣的部分:
- 使用ES6计算属性语法,因此可以用简洁的方式通过
Object.assign()
来更新state[action.subreddit]
。如下:return Object.assign({}, state, { [action.subreddit]: posts(state[action.subreddit], action) })
其等价于:
let nextState = {} nextState[action.subreddit] = posts(state[action.subreddit], action) return Object.assign({}, state, nextState)
- 我们提取了管理特定帖子列表状态的
'posts(state, action)
,其只是reducer的组成部分。如何将reducer拆分为较小的reducer,这种情况下,我们会将对象内部的更新项目分发给posts
reducer。实际示例中更进一步,展示了如何为参数化分页reducer创建reducer工厂。
请记住,reducer只是函数,因此可以随意使用函数组合和高阶函数。
异步Action构造函数(Action Creator)
最后,我们会将前面定义的同步action creator与网络请求一起使用,Redux的标准实现方式是使用Redux Thunk中间件。它有一个称为redux-thunk
的独立包。稍后我们将解释中间件的一般工作方式。目前只需要了解一件事:通过使用这一中间件,action creator可以返回函数而不是action对象。这样,action creator就成为了一个thunk。
当action creator返回一个函数时,该函数将由Redux Thunk中间件执行。此函数不必是纯函数,所以它会有副作用,包括执行异步API调用。该函数还可以调度动作(action),如我们前面定义的那些同步动作。
我们仍可以在actions.js
文件中定义这些特殊的thunk action creator:
actions.js
(异步)
import fetch from 'cross-fetch' export const REQUEST_POSTS = 'REQUEST_POSTS' function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit } } export const RECEIVE_POSTS = 'RECEIVE_POSTS' function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map(child => child.data), receivedAt: Date.now() } } export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' export function invalidateSubreddit(subreddit) { return { type: INVALIDATE_SUBREDDIT, subreddit } } // 第一个thunk action creator! // 虽然其内部可能不同,但可以像其它普通action creator一样使用: // store.dispatch(fetchPosts('reactjs')) export function fetchPosts(subreddit) { // Thunk中间件知道如何处理函数。 // 它将调度方法作为参数传递给函数,从而使其能够调度action本身。 return function(dispatch) { // 首个: 应用状态(state)会更新, // 以通知API调用已开始. dispatch(requestPosts(subreddit)) // Thunk中间件调用的函数可以返回一个值, // 该值将作为调度方法的返回值传递。 // 在这种情况下,我们返回一个等待状态的promise。 // thunk中间件不需要这样做,但是对我们来说很方便。 return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then( response => response.json(), // 不要使用catch,因为那样也会捕获 // 调度和生成的渲染中的任何错误, // 并导致“Unexpected batch number”错误循环。 // https://github.com/facebook/react/issues/6895 error => console.log('An error occurred.', error) ) .then(json => // 我们可以分发多次!在这里,使用API调用的结果更新应用状态。 dispatch(receivePosts(subreddit, json)) ) } }
关于fetch
在上例中我们使用了fetch
API。它是用于发送网络请求的新API,可用于代替XMLHttpRequest
满足大多数常见网络请求。因为有些浏览器尚不支持,所以建议使用cross-fetch
库:
// Do this in every file where you use `fetch` import fetch from 'cross-fetch'
在该库内部,会客户端上使用whatwg-fetch
polyfill,而在服务端会使用node-fetch
,因此,如果将应用更改为通用应用,也无需更修改API调用。
请注意,任何fetch
polyfill都假定已存在Promise polyfill。确保已有Promise polyfill的最简单方法是,在运行任何其他代码前在程度入口点启用Babel的ES6 polyfill:
// Do this once before any other code in your app import 'babel-polyfill'
接下来,我们如何在调度(dispatch)机制中包括Redux Thunk中间件?使用Redux的applyMiddleware()
存储增强器,如下所示:
index.js
import thunkMiddleware from 'redux-thunk' import { createLogger } from 'redux-logger' import { createStore, applyMiddleware } from 'redux' import { selectSubreddit, fetchPosts } from './actions' import rootReducer from './reducers' const loggerMiddleware = createLogger() const store = createStore( rootReducer, applyMiddleware( thunkMiddleware, // lets us dispatch() functions loggerMiddleware // neat middleware that logs actions ) ) store.dispatch(selectSubreddit('reactjs')) store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState()))
使用thunk的好处在于它们可以相互分配结果:
actions.js
(使用fetch
)
import fetch from 'cross-fetch' export const REQUEST_POSTS = 'REQUEST_POSTS' function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit } } export const RECEIVE_POSTS = 'RECEIVE_POSTS' function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map(child => child.data), receivedAt: Date.now() } } export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' export function invalidateSubreddit(subreddit) { return { type: INVALIDATE_SUBREDDIT, subreddit } } function fetchPosts(subreddit) { return dispatch => { dispatch(requestPosts(subreddit)) return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json))) } } function shouldFetchPosts(state, subreddit) { const posts = state.postsBySubreddit[subreddit] if (!posts) { return true } else if (posts.isFetching) { return false } else { return posts.didInvalidate } } export function fetchPostsIfNeeded(subreddit) { // Note that the function also receives getState() // which lets you choose what to dispatch next. // This is useful for avoiding a network request if // a cached value is already available. return (dispatch, getState) => { if (shouldFetchPosts(getState(), subreddit)) { // Dispatch a thunk from thunk! return dispatch(fetchPosts(subreddit)) } else { // Let the calling code know there's nothing to wait for. return Promise.resolve() } } }
这使我们可以使用几乎相同的代码,来编写逐渐变得更复杂的异步控制流:
store .dispatch(fetchPostsIfNeeded('reactjs')) .then(() => console.log(store.getState()))
关于服务端渲染
异步action creator对于服务端渲染特别方便。你可以创建一个store,调度(dispatch)一个异步action creator,然后再调度其他异步action creator以获取应用的全部数据,并且仅在Promise返回完成后才进行渲染。这样,store将在渲染之前已被充填为所需要的状态。
Thunk中间件不是Redux中协调异步操作的唯一方法:
- 使用redux-promise或redux-promise-middleware来调度Promises而不是函数。
- 使用redux-observable来调度Observables.
- 使用redux-saga中间件来构建更复杂的异步actions
- 使用redux-pack中间件来调度基于promise的异步操作。
- 还可以编写自定义中间件来描述对API的调用,如实际示例所示
无论有没有中间件,都可以尝试选择自己喜欢的约定并遵循它。
连接到UI
调度异步操作与调度同步操作没有什么不同,因此不再详细讨论。有关在React组件中使用Redux的介绍,请参见:React的用法。有关此示例中的完整源代码,请参见示例:Reddit API。
3.2 异步数据流
没有中间件,Redux store仅支持同步数据流。这是默认通过createStore()
所获得的内容。
可以使用applyMiddleware()
来增强createStore()
。这不是必需的,但是它使你可以方便地表达异步操作。
诸如redux-thunk或redux-promise之类的异步中间件包装了store的dispatch()
方法,并允许你分发action之外的其他东西,如:函数或Promises。然后,所使用的任何中间件都可以拦截所分发的所有内容,进而可以将操作传递给链中的下一个中间件。例如,一个Promise中间件可以拦截Promise,并响应每个Promise异步调度一对开始/结束action。
当链中的最后一个中间件调度一个action时,其必须是一个普通对象。这是同步Redux数据流发生的时间。
查看异步示例的完整源代码。
3.3 中间件(Middleware)
在“异步 Action”示例中我们已经看到了中间件的实际应用。如果你使用过Express和Koa之类的服务端库,那么你可能已经很熟悉中间件的概念。在这些框架中,中间件是一些可以在接收请求的框架与生成响应的框架之间放置的代码。例如:Express或Koa中间件可能会添加CORS标头、日志记录、压缩等。中间件最大特点是它们是可组合的,你可以在一个项目中使用多个独立的第三方中间件。
而Redux中间件解决了与Express或Koa中间件不同的问题,但是在概念上是类似的。它在调度action与到达reducer之间提供了第三方扩展点。通过使用Redux中间件,可以进行日志记录、崩溃报告、异步API会话、路由等等。
本节将对其做一个较深入的介绍以帮助理解,并提供了一些实际的示例来展示中间件的功能。
理解中间件
中间件的用途很多,包括异步API调用。为了更好的了解中间件,我们将以日志记录和崩溃报告为例,介绍中间件的工作流程。
问题:日志记录
Redux的好处之一是它使状态(state)更改可预测且透明。每次调度动作(action)时,都会计算并保存新状态。状态本身不能改变,它只能由于特定动作而改变。
根据这一特性,使其可以很方便的用于日志记录。当出现问题时,我们根据日志找到哪个操作破坏了状态。
尝试 #1:手工记录
最基本的解决方案是每次调用store.dispatch(action)
时都要记录操作,然后自己记录下一个状态。这并不是最终方案,而只是了解问题的第一步。
如,在添加todo时:
store.dispatch(addTodo('Use Redux'))
要记录action和state,可以将其修改如下:
const action = addTodo('Use Redux') console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState())
这样已经可以达到我们想要的效果,但是我们并不想每次都这样做。
尝试 #2:封装 Dispatch
可以将日志记录提取到一个函数中:
function dispatchAndLog(store, action) { console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState()) }
然后将其替代store.dispatch()
:
dispatchAndLog(store, addTodo('Use Redux'))
到这里其实已经可以结束了,但是每次导入一个特殊函数并不是很方便。
尝试 #3:Monkeypatching Dispatch
如果我们仅替换store实例上的dispatch
函数怎么办?Redux store只是带有一些方法的普通对象,我们正在编写JavaScript,因此我们可以对dispatch
使用猴子补丁(Monkey patch)的方式实现:
const next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result }
这已经接近我们想要的了!无论我们在何处调度action,都可以保证将其记录下来。Monkeypatching感觉上也不对,但已可以忍受。
问题:崩溃报告
如果我们想应用多个这样的转换来dispatch
怎么办?
还有一种对JavaScript运行中错误的处理。全局的window.onerror
事件并不可靠,因为它在某些较旧的浏览器中没有堆栈信息,这对于找到发生错误至关重要。
如果在任何时候由于调度操作而引发错误,都将其发送到崩溃报告服务(如:Sentry),并带有堆栈跟踪、引发错误的action以及当前state,这样就更容易复现开发中的错误。
在实际处理中,我们还需要分开记录日志和崩溃报告。理想情况下,我们希望它们是不同的模块,可能还在不同的包中。但这可能与我们正在介绍的中间件的初衷可能不符。
通过工具方法记录日志和崩溃报告时,可能如下所示:
function patchStoreToAddLogging(store) { const next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } function patchStoreToAddCrashReporting(store) { const next = store.dispatch store.dispatch = function dispatchAndReportErrors(action) { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } }
如果这些功能作为单独的模块发布,我们以后可以使用它们来修改store:
patchStoreToAddLogging(store) patchStoreToAddCrashReporting(store)
尝试 #4:隐藏 Monkeypatching
Monkeypatching是一个hack,可以用来“替换你喜欢的任何方法”。 之前,我们的函数替换了store.dispatch
,但如果他们返回了新的dispatch
函数怎么办?
function logger(store) { const next = store.dispatch // Previously: // store.dispatch = function dispatchAndLog(action) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }
我们可以在Redux内部提供一个帮助程序,该帮助程序将实现Monkeypatching应用的细节:
function applyMiddlewareByMonkeypatching(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() // Transform dispatch function with each middleware. middlewares.forEach(middleware => (store.dispatch = middleware(store))) }
当需要应用多个中间件时:
applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
但是,它仍然是个猴子补丁程序,我们只是将其隐藏在了库中。
尝试 #5:移除 Monkeypatching
为什么要重写甚至覆盖dispatch
?还有一个原因:每个中间件都可以访问(并调用)以前包装的store.dispatch
:
function logger(store) { // Must point to the function returned by the previous middleware: const next = store.dispatch return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }
链接中间件非常重要。
如果applyMiddlewareByMonkeypatching
在处理第一个中间件后没有立即分配store.dispatch
,那么配store.dispatch
将继续指向原始分配函数。然后,第二个中间件也将绑定到原始的dispatch
函数。
还有另一种启用链接的方法。中间件可以接受next()
调度函数作为参数,而不是从store实例中读取它。
function logger(store) { return function wrapDispatchToAddLogging(next) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } }
这是一个“我们需要更深入”的时刻,因此可能需要一段时间才会使之有意义。级联功能令人生畏,这时可以通过ES6箭头函数使此操作更易理解:
const logger = store => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } }
这正是Redux中间件的样子。
现在,中间件使用next()
做为调度函数,并返回一个dispatch
函数,该函数又充当左侧中间件的next()
,依此类推。store中的方法(诸如getState()
)仍然很有用,因此store
仍可作为顶级参数使用。
尝试 #6:单纯使用中间件
为代替applyMiddlewareByMonkeypatching()
可以编写applyMiddleware()
,该方法首先获得最终的、完全包装的dispatch()
函数,并使用该函数返回store的副本:
// Warning: Naïve implementation! // That's *not* Redux API. function applyMiddleware(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() let dispatch = store.dispatch middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch))) return Object.assign({}, store, { dispatch }) }
与Redux中的applyMiddleware()
的实现类似,但有三方面不同:
它仅向中间件公开store API的子集:
dispatch(action)
andgetState()
。确保从中间件而不是
next(action)
调用store.dispatch(action)
需要一些技巧,该操作实际上将再次遍历整个中间件链,包括当前的中间件。如前所述,这对于异步中间件很有用。在安装过程中调用dispatch
时有一个警告,如下所述。为了确保只应用一次中间件,该中间件只能在
createStore()
上运行,而不是在store
上。它的签名不是(store, middlewares) => store
,而是(...middlewares) => (createStore) => createStore
。
因为在使用函数之前先将函数应用于createStore()
很麻烦,所以createStore()
接受一个可选的last
参数来指定此类函数。
警告:安装过程中调度
当applyMiddleware
执行并设置你的中间件时,store.dispatch
函数将指向createStore
提供的原始版本。调度会导致不应用其他中间件,并引起不能在安装过程中与其他中间件进行交互的问题。由于这种意外行为,如果尝试在设置完成之前调度action,则applyMiddleware
将引发错误;为避免问题,应通过一个公共对象(对于调用API的中间件,可能是你的API客户端对象)直接与其他中间件进行通信,或者等到使用回调构造中间件之后再进行通信。
最终方案
有了这个中间件,我们就可以:
const logger = store => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } }
将其应用到Redux store中:
import { createStore, combineReducers, applyMiddleware } from 'redux' const todoApp = combineReducers(reducers) const store = createStore( todoApp, // applyMiddleware() tells createStore() how to handle middleware applyMiddleware(logger, crashReporter) )
现在,分发给store实例的所有acton都将通过logger
和crashReporter
传递:
// Will flow through both logger and crashReporter middleware! store.dispatch(addTodo('Use Redux'))
示例
以下是一些有效的Redux中间件示例,它们不一定适用于你的项目,但可参考使用:
/** * 记录所有`action`和`state`,然后将其分发(dispatch) */ const logger = store => next => action => { console.group(action.type) console.info('dispatching', action) let result = next(action) console.log('next state', store.getState()) console.groupEnd() return result } /** * 在`state`更新并通知监听器时,发送崩溃报告。 */ const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } /** * 将`{ meta: { delay: N } }`的`action`计划为延迟N毫秒。 * 这种情况下,使`dispatch`返回一个取消超时的函数。 */ const timeoutScheduler = store => next => action => { if (!action.meta || !action.meta.delay) { return next(action) } const timeoutId = setTimeout(() => next(action), action.meta.delay) return function cancel() { clearTimeout(timeoutId) } } /** * 计划带有`{ meta: { raf: true } }`的`action`在'rAF'循环框架内调度。 * 在这种情况下,使`dispatch`返回一个函数将操作从队列中删除。 */ const rafScheduler = store => next => { const queuedActions = [] let frame = null function loop() { frame = null try { if (queuedActions.length) { next(queuedActions.shift()) } } finally { maybeRaf() } } function maybeRaf() { if (queuedActions.length && !frame) { frame = requestAnimationFrame(loop) } } return action => { if (!action.meta || !action.meta.raf) { return next(action) } queuedActions.push(action) maybeRaf() return function cancel() { queuedActions = queuedActions.filter(a => a !== action) } } } /** * 除了`action`外,还可以分发'promise'。 * 如果'promise'能够'resolved',其结果将作为一个`action`发送。 * 'promise'是从`dispatch`返回的,因此调用者可以处理拒绝 */ const vanillaPromise = store => next => action => { if (typeof action.then !== 'function') { return next(action) } return Promise.resolve(action).then(store.dispatch) } /** * 使你可以使用`{ promise }`字段调度特殊`action`。 * 这个中间件在开始时会将它们变成单个`action`, * 并在'promise'解决后执行一次成功(或失败)操作。 * 为了方便起见,`dispatch`将返回'promise',以便调用者可以等待处理。 */ const readyStatePromise = store => next => action => { if (!action.promise) { return next(action) } function makeAction(ready, data) { const newAction = Object.assign({}, action, { ready }, data) delete newAction.promise return newAction } next(makeAction(false)) return action.promise.then( result => next(makeAction(true, { result })), error => next(makeAction(true, { error })) ) } /** * 让你调度一个函数而不是一个`action`。 * 此函数将接收`dispatch`和`getState`作为参数。 * 对于提前退出(在`getState()`上的条件)也很有用, * 同样适用于异步控制流(可以`dispatch()`其他的方式)。 * `dispatch`将返回调度函数的返回值。 */ const thunk = store => next => action => typeof action === 'function' ? action(store.dispatch, store.getState) : next(action) // 也可以一块使用他们(如果需求) const todoApp = combineReducers(reducers) const store = createStore( todoApp, applyMiddleware( rafScheduler, timeoutScheduler, thunk, vanillaPromise, readyStatePromise, logger, crashReporter ) )
3.4 配合React Router使用
要使用Redux应用使用路由,可以将其与React Router一起使用。Redux会成为数据的实际来源,而React Router会成为URL的真实来源。在大多数情况下,应该将它们分开,除非需要定时或重新触发URL更改的操作。
安装React Router
react-router-dom
是一个npm包。本章节假定使用react-router-dom@^4.1.1
。
npm install --save react-router-dom
配置 Fallback URL
在集成React Router之前,需要配置我们的开发服务器。实际上,我们的开发服务器并不知道到React Router配置中声明的路由。例如,如果访问/todos
并刷新,则需要开发服务器提供index.html
,因为它是一个单页应用。以下是是一个流行的开发服务器的实现。
配置Express
如果通过Express提供index.html
:
app.get('/*', (req, res) => { res.sendFile(path.join(__dirname, 'index.html')) })
配置 WebpackDevServer
如果通过WebpackDevServer提供index.html
,则在webpack.config.dev.js
文件中添加如下配置:
devServer: { historyApiFallback: true }
连接React Router和Redux App
在本章中,我们将使用Todos示例。建议在阅读本章时克隆它。
首先,我们需要从React Router导入<Router />
和<Route />
import { BrowserRouter as Router, Route } from 'react-router-dom'
在React应用中,通常会将<Route />
包装到<Router />
中,以便以URL更改时<Router />
将其匹配到对应的路由分支,并渲染所配置的组件。<Route />
用于以声明方式将路由映射到应用组件的层次结构。需要在path
中声明所要使用的路径,并在组件中声明路径与Web匹配时要渲染的单个组件。
const Root = () => ( <Router> <Route path="/" component={App} /> </Router> )
在我们的Redux应用中,仍然需要<Provider />
。<Provider />
是一个高阶组件,由React Redux提供,使你可以将Redux绑定到React。(参考:结合React使用)
从React Redux中导入<Provider />
:
import { Provider } from 'react-redux'
我们会将<Router />
包装到<Provider />
中,这样路由就可以访问store
:
const Root = ({ store }) => ( <Provider store={store}> <Router> <Route path="/" component={App} /> </Router> </Provider> )
现在,如果URL匹配/
,则将渲染<App />
组件。另外,我们会为/
添加可选的:filter?
参数,以便从其读取参数:
<Route path="/:filter?" component={App} />
components/Root.js
import React from 'react' import PropTypes from 'prop-types' import { Provider } from 'react-redux' import { BrowserRouter as Router, Route } from 'react-router-dom' import App from './App' const Root = ({ store }) => ( <Provider store={store}> <Router> <Route path="/:filter?" component={App} /> </Router> </Provider> ) Root.propTypes = { store: PropTypes.object.isRequired } export default Root
我们还需要重构index.js
,以将染<root />
组件渲染给DOM。
index.js
import React from 'react' import { render } from 'react-dom' import { createStore } from 'redux' import todoApp from './reducers' import Root from './components/Root' const store = createStore(todoApp) render(<Root store={store} />, document.getElementById('root'))
通过 React Router 导航
React Router组件通过<Link />
提供应用内的导航。如果要添加样式,react-router-dom
还有一个特殊的<Link />
,称为<NavLink />
,它可以接受样式属性。例如,可以在激活状态上添加activeStyle
样式。
在我们的示例中,可以用新的容器组件<FilterLink />
包装<NavLink />
,以便动态更改URL。
containers/FilterLink.js
import React from 'react' import { NavLink } from 'react-router-dom' const FilterLink = ({ filter, children }) => ( <NavLink exact to={filter === 'SHOW_ALL' ? '/' : `/${filter}`} activeStyle={{ textDecoration: 'none', color: 'black' }} > {children} </NavLink> ) export default FilterLink
components/Footer.js
import React from 'react' import FilterLink from '../containers/FilterLink' import { VisibilityFilters } from '../actions' const Footer = () => ( <p> Show: <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink> {', '} <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink> </p> ) export default Footer
我们点击<FilterLink />
时,会看到URL在'/SHOW_COMPLETED'
、'/SHOW_ACTIVE'
和'/'
之间变化。即使通过浏览器返回,它也会使用浏览器的历史记录并有效地转到以前的URL。
从URL中读取数据
现在,即使更改了URL,也不会过滤todo列表。因为我们正从<VisibleTodoList />
的mapStateToProps()
进行过滤,其仍绑定到state
而不是URL。mapStateToProps
有可选的第二个参数ownProps
,它是一个对象,每个props都会传给<VisibleTodoList />
。
containers/VisibleTodoList.js
const mapStateToProps = (state, ownProps) => { return { todos: getVisibleTodos(state.todos, ownProps.filter) // previously was getVisibleTodos(state.todos, state.visibilityFilter) } }
现在我们没有向<App />
传递任何东西,所以ownProps
是一个空对象。如果通过URL过滤todo列表,可以向<VisibleTodoList />
传递URL参数。
之前我们写过:<Route path="/:filter?" component={App} />
,其可能向<App />
添加一个params
属性。
params
属性是一个对象,其在URL中使用match
对象指定了每个参数。如:当我们导航到localhost:3000/SHOW_COMPLETED
时,match.params
将等于{ filter: 'SHOW_COMPLETED' }
。
components/App.js
const App = ({ match: { params } }) => { return ( <div> <AddTodo /> <VisibleTodoList filter={params.filter || 'SHOW_ALL'} /> <Footer /> </div> ) }
下一步
现在,我们已经知道如何使用基本路由,可以通过React Router API了解更多相关信息。
3.5 示例: Reddit API
以下是我们在高级教程中构建的“Reddit标题获取”示例的完整源代码。
入口点
index.js
import 'babel-polyfill' import React from 'react' import { render } from 'react-dom' import Root from './containers/Root' render(<Root />, document.getElementById('root'))
Action构造函数(Action Creators)和常量
actions.js
import fetch from 'cross-fetch' export const REQUEST_POSTS = 'REQUEST_POSTS' export const RECEIVE_POSTS = 'RECEIVE_POSTS' export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT' export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT' export function selectSubreddit(subreddit) { return { type: SELECT_SUBREDDIT, subreddit } } export function invalidateSubreddit(subreddit) { return { type: INVALIDATE_SUBREDDIT, subreddit } } function requestPosts(subreddit) { return { type: REQUEST_POSTS, subreddit } } function receivePosts(subreddit, json) { return { type: RECEIVE_POSTS, subreddit, posts: json.data.children.map(child => child.data), receivedAt: Date.now() } } function fetchPosts(subreddit) { return dispatch => { dispatch(requestPosts(subreddit)) return fetch(`https://www.reddit.com/r/${subreddit}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(subreddit, json))) } } function shouldFetchPosts(state, subreddit) { const posts = state.postsBySubreddit[subreddit] if (!posts) { return true } else if (posts.isFetching) { return false } else { return posts.didInvalidate } } export function fetchPostsIfNeeded(subreddit) { return (dispatch, getState) => { if (shouldFetchPosts(getState(), subreddit)) { return dispatch(fetchPosts(subreddit)) } } }
Reducer
reducers.js
import { combineReducers } from 'redux' import { SELECT_SUBREDDIT, INVALIDATE_SUBREDDIT, REQUEST_POSTS, RECEIVE_POSTS } from './actions' function selectedSubreddit(state = 'reactjs', action) { switch (action.type) { case SELECT_SUBREDDIT: return action.subreddit default: return state } } function posts( state = { isFetching: false, didInvalidate: false, items: [] }, action ) { switch (action.type) { case INVALIDATE_SUBREDDIT: return Object.assign({}, state, { didInvalidate: true }) case REQUEST_POSTS: return Object.assign({}, state, { isFetching: true, didInvalidate: false }) case RECEIVE_POSTS: return Object.assign({}, state, { isFetching: false, didInvalidate: false, items: action.posts, lastUpdated: action.receivedAt }) default: return state } } function postsBySubreddit(state = {}, action) { switch (action.type) { case INVALIDATE_SUBREDDIT: case RECEIVE_POSTS: case REQUEST_POSTS: return Object.assign({}, state, { [action.subreddit]: posts(state[action.subreddit], action) }) default: return state } } const rootReducer = combineReducers({ postsBySubreddit, selectedSubreddit }) export default rootReducer
Store
configureStore.js
import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import { createLogger } from 'redux-logger' import rootReducer from './reducers' const loggerMiddleware = createLogger() export default function configureStore(preloadedState) { return createStore( rootReducer, preloadedState, applyMiddleware(thunkMiddleware, loggerMiddleware) ) }
容器组件
containers/Root.js
import React, { Component } from 'react' import { Provider } from 'react-redux' import configureStore from '../configureStore' import AsyncApp from './AsyncApp' const store = configureStore() export default class Root extends Component { render() { return ( <Provider store={store}> <AsyncApp /> </Provider> ) } }
containers/AsyncApp.js
import React, { Component } from 'react' import PropTypes from 'prop-types' import { connect } from 'react-redux' import { selectSubreddit, fetchPostsIfNeeded, invalidateSubreddit } from '../actions' import Picker from '../components/Picker' import Posts from '../components/Posts' class AsyncApp extends Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) this.handleRefreshClick = this.handleRefreshClick.bind(this) } componentDidMount() { const { dispatch, selectedSubreddit } = this.props dispatch(fetchPostsIfNeeded(selectedSubreddit)) } componentDidUpdate(prevProps) { if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) { const { dispatch, selectedSubreddit } = this.props dispatch(fetchPostsIfNeeded(selectedSubreddit)) } } handleChange(nextSubreddit) { this.props.dispatch(selectSubreddit(nextSubreddit)) this.props.dispatch(fetchPostsIfNeeded(nextSubreddit)) } handleRefreshClick(e) { e.preventDefault() const { dispatch, selectedSubreddit } = this.props dispatch(invalidateSubreddit(selectedSubreddit)) dispatch(fetchPostsIfNeeded(selectedSubreddit)) } render() { const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props return ( <div> <Picker value={selectedSubreddit} onChange={this.handleChange} options={['reactjs', 'frontend']} /> <p> {lastUpdated && ( <span> Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '} </span> )} {!isFetching && ( <button onClick={this.handleRefreshClick}>Refresh</button> )} </p> {isFetching && posts.length === 0 && <h2>Loading...</h2>} {!isFetching && posts.length === 0 && <h2>Empty.</h2>} {posts.length > 0 && ( <div style={{ opacity: isFetching ? 0.5 : 1 }}> <Posts posts={posts} /> </div> )} </div> ) } } AsyncApp.propTypes = { selectedSubreddit: PropTypes.string.isRequired, posts: PropTypes.array.isRequired, isFetching: PropTypes.bool.isRequired, lastUpdated: PropTypes.number, dispatch: PropTypes.func.isRequired } function mapStateToProps(state) { const { selectedSubreddit, postsBySubreddit } = state const { isFetching, lastUpdated, items: posts } = postsBySubreddit[ selectedSubreddit ] || { isFetching: true, items: [] } return { selectedSubreddit, posts, isFetching, lastUpdated } } export default connect(mapStateToProps)(AsyncApp)
展示组件
components/Picker.js
import React, { Component } from 'react' import PropTypes from 'prop-types' export default class Picker extends Component { render() { const { value, onChange, options } = this.props return ( <span> <h1>{value}</h1> <select onChange={e => onChange(e.target.value)} value={value}> {options.map(option => ( <option value={option} key={option}> {option} </option> ))} </select> </span> ) } } Picker.propTypes = { options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired }
components/Posts.js
import React, { Component } from 'react' import PropTypes from 'prop-types' export default class Posts extends Component { render() { return ( <ul> {this.props.posts.map((post, i) => ( <li key={i}>{post.title}</li> ))} </ul> ) } } Posts.propTypes = { posts: PropTypes.array.isRequired }
3.6 下一步
阅读到本节,你可能会想知道“现在我该怎么办?”。在这里,我们将提供一些基本的技巧/建议,以帮助你了解从创建TodoMVC应用到现实世界应用不同。
实际项目中的建议和思考
在我们决定创建一个新项目时,我们都会绕过某些将来可能影响我们效率的东西。在实际项目中,开始编码前我们必须考虑几件事,例如:如何配置store
、store
的大小、数据结构、状态(state
)模型、中间件、环境、异步事务、持久化等。
以上是我们必须事先考虑的一些因素。这不是一件容易的事,但是有一些策略可以使之顺利进行。
UI 与 State
开发人员在使用Redux时所面临的最大挑战之一是用数据描述UI状态。大多数软件只是数据转换,并且清楚地了解UI只是渲染数据,从而促进构建过程。
Nicolas Hery在用数据描述UI状态中很好地描述这点。另外,知道何时使用Redux是很好的,因为很多时候你可能不需要Redux。
Store配置
要配置store
,我们必须要考虑要使用哪种中间件。以下是几个比较受欢迎的库:
异步调度相关:
- redux-thunk
- Redux Thunk中间件使你可以编写返回函数而不是action的action creator。thunk可用于延迟动作的调度,或者仅在满足特定条件时才调度。它合并了方法
dispatch
和getState
作为参数。
- Redux Thunk中间件使你可以编写返回函数而不是action的action creator。thunk可用于延迟动作的调度,或者仅在满足特定条件时才调度。它合并了方法
- redux-saga
redux-saga
是一个库,旨在使应用副作用(例如:数据提取等异步任务和访问浏览器缓存等非常规操作)的执行可管理且高效。测试很简单,因为它使用了称为generators
的ES6功能,从而使流程像同步代码一样易于阅读。
- redux-observable
redux-observable
受redux-thunk
启发的Redux中间件。它允许开发人员调度一个函数,该函数返回Observable
、Promise
或Iterable
的action。当observable
对象发出一个action,或promise成功后一个action,或iterable
对象发出一个action时,该动作将照常调度。
开发/调试相关:
- redux-devtools
- Redux DevTools是用于Redux开发工作流程的一组工具。
- redux-logger
redux-logger
会记录所有调度到store的action。
要在这些库中选择,我们需要是在构建小型应用还是大型应用。也需要考虑可用性、代码标准和JavaScript知识。
命名规范
大型项目的一大困惑在于如何命名。这与代码本身一样重要,并且在项目一开始就需要为action定义一个命名约定,遵循该约定可以帮助你随着项目范围的扩大而更好的组织代码。
参考示例:Redux中Action Creator的简单命名约定和Redux模式和反模式。
可扩展性
分析和预测你应用的增长并没有什么魔法。不过没关系!Redux的简单化基础意味着它可以适应各种应用的增长。以下是一些关于如何以合理的方式构建应用的相关资源:
综上所述,最佳实践的是保持编码和学习。参与issues和StackOverFlow 上的问题,也是掌握Redux的一个好方法。
4. API
- 4.1 概述
- 4.2
createStore
- 4.3 Store
- 4.4
combineReducers
- 4.5
applyMiddleware
- 4.6
bindActionCreators
- 4.7
compose
4.1 概述
Redux的API很少。Redux定义了一系列需要你自己实现约定(如:reduce),并提供了少量辅助函数以将这些约定整合在一起。
本部分包含了完整的Redux API。请记住,Redux 仅管理状态(state)。在实际应用中,还需要使用UI绑定库(如:react-redux)。
顶级导出方法
createStore(reducer, [preloadedState], [enhancer])
combineReducers(reducers)
applyMiddleware(...middlewares)
bindActionCreators(actionCreators, dispatch)
compose(...functions)
Store API
导入
上述的每个函数都是顶层导出。这样你就可以这样导入其中的任何一个:
ES6
import { createStore } from 'redux'
ES5(CommonJS)
var createStore = require('redux').createStore
ES5(UMD)
var createStore = Redux.createStore
4.2 createStore(reducer, [preloadedState], [enhancer])
创建一个Redux store,其中包含应用的完整状态树。在你应用中应该仅一个store。
参数
reducer
(Function):reducer
函数。其接受当前状态(state)树及要处理的action,并返回下一状态树[
preloadedState
] (any):初始状态。可以指定其值,以在同构应用中混合服务端状态,或还原之前的用户会话。如果你用CombineReducers
创建了reducer
,那么它必须是一个普通对象,并与传入的key结构相同。或传入reducer
可以解析的所有内容。[
enhancer
] (Function):Store 的扩展器。可以通过它来扩展第三方功能,如中间件、持久化等。Redux附带的唯一store enhancer是applyMiddleware()
。
返回值
(Store
):包含应用完整状态的对象。更改其状态的唯一方法是调度操作(dispatch action)。也可以监听对其状态的更改以更新UI。
示例
import { createStore } from 'redux' function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return state.concat([action.text]) default: return state } } const store = createStore(todos, ['Use Redux']) store.dispatch({ type: 'ADD_TODO', text: 'Read the docs' }) console.log(store.getState()) // [ 'Use Redux', 'Read the docs' ]
提示
- 在一个应用中不要创建多个store!相反,可以使用
combineReducers
从多个reducer中创建一个根reducer。 - 你可以选择state格式。可以使用普通对象或类似不可变的对象。如果不确定,建议使用普通对象。
- 如果你的state是普通对象,请确保永远不要对其进行修改!例如,不要从reducer中返回类似
Object.assign(state, newData)
这样的东西,而是返回Object.assign({}, state, newData)
。这样,就不会覆盖之前的state
。还可以使用对象扩展运算符,如return { ...state, ...newData }
。 - 对于在服务端上运行的通用应用,请为每个请求创建一个store实例,以在应用之间进行隔离。向store实例分发一些action以获取数据,然后等它们完成,请求完成后再在服务器上渲染应用。
- 创建store后,Redux会向reducer分配一个虚拟action,以使用初始state填充store。无需直接处理虚拟action。只要记住,如果作为第一个参数传给它的state是
undefined
,那么你的reducer应该返回某种初始状态,并且你已全部设置。 - 要应用多个store增强器,可以使用compose。
4.3 Store
Store拥有应用的整个状态树。更改其内部state的唯一方法是在其上调度操作(dispatch action)。
Store不是类,只是一个带有方法的对象。要创建它,请求根reducer函数传给createStore
。
Flux用户注意事项如果你之前使用Flux,那么需要了解一个重要的区别。Redux没有Dispatcher也不支持多个store。相反,只有单个store其有一个reducer函数。随着应用的增长,无需分拆根store,而是将根reducer拆分为较小的reducer,分别在状态树的不同部分操作。可以使用诸如combineReducers之类的辅助方法来组合。这与React应用中只有一个根组件的情况类似。
Store 中的方法
getState()
返回应用的当前状态树。它与store的reducer返回的最后一个值相同。
返回值
(any):应用的当前状态树
dispatch(action)
调度操作。这是触发state更新的唯一方法。
将使用当前的getState()
结果和传入的action以同步的方式调用reduce函数。其返回值将作为下一个state。这时,它将从getState()
返回,并将立即通知更新监听器。
参数
action
(Object†):描述应用变化的普通对象。Action 是将数据存储到store中的唯一方法,因此,无论是来自UI事件、网络回调还是其它源(如:WebSockets),最终都要以action的形式分发(dispatch)。Action必须有指示要执行的动作类型的type
字段。type
可以定义为常量,并可以从另一个模块导入。使用字符串作为类型要好于使用符号,因为字符串是可序列化的。除了type
外,action
对象的结构完全由你定义。更多相关信息,请参考:Flux标准操作。
返回值
(Object†):所分发的action。(参考下方“注意”)
注意
†通过调用createStore
获取的“原始”store实现仅支持普通对象操作,并将其立即传入reducer
但是,如果使用applyMiddleware
包装createStore
,则中间件可以修改action的执行,并为分发异步action提供支持。异步action通常使用异步原语,如:Promise
、Observable
或thunk
。
中间件是由社区开发,默认情况下不随Redux一起提供。你需要显式安装像redux-thunk或redux-promise这样的库才能使用。也可以创建自己的中间件。
示例
import { createStore } from 'redux' const store = createStore(todos, ['Use Redux']) function addTodo(text) { return { type: 'ADD_TODO', text } } store.dispatch(addTodo('Read the docs')) store.dispatch(addTodo('Read about the middleware'))
subscribe(listener)
添加修改监听器。每当分发action时,就会调用它,并且状态树的某些部分可能已更改。你可以调用getState()
以读取回调中的当前状态树。
可以从修改监听器调用dispatch()
。但要注意以下几点:
- 监听器仅应响应用户动作(action)或在特定条件下(如,当store有特定字段时分发action)来调用
dispatch()
。从技术上讲,可以在没有任何条件的情况下调用dispatch()
,但是由于每个dispatch()
调用通常都会再次触发监听器,并导致无限循环。 - 监听将在每次
dispatch()
调用之前进行快照。如果在调用监听器时进行监听或取消监听,这不会对当前正在进行的dispatch()
产生任何影响。但是,下一个dispatch()
调用(无论是否嵌套)将使用监听列表的最新快照。 - 监听器不应关注所有状态更改,因为在调用监听器之前,该状态可能已在嵌套
dispatch()
期间多次更新。但是,可以确保所有监听器都在dispatch()
启动之前,这样调用监听器时,就会传入监听器存在时间内的最新状态(state)。
这是一个低层的API。多数情况下你不会直接使用它,而是使用React(或其它绑定)。如果你使用回调做对状态修改的钩子,那么可能需要使用observeStore
工具。
要取消监听“修改”监听器,则调用subscribe
返回的函数。
参数
listener
(Function):每当分发动作且状态树可能已更改时,都将调用该回调。你可以在此回调中调用getState()
以读取当前状态树。可以预测store的reducer是纯函数,因此可以将对状态树中某个深层路径的引用进行比较,以了解其值是否已更改。
返回值
(Function):取消监听“修改监听器”的函数。
返回值
function select(state) { return state.some.deep.property } let currentValue function handleChange() { let previousValue = currentValue currentValue = select(store.getState()) if (previousValue !== currentValue) { console.log( 'Some deep nested property changed from', previousValue, 'to', currentValue ) } } const unsubscribe = store.subscribe(handleChange) unsubscribe()
replaceReducer(nextReducer)
替换store当前用于计算state的reducer。
这是一个高级API。如果你的应用程序使用了代码拆分,并且要动态地加载某些reducer,则可能需要这样做;或者为Redux实现热重载机制,也可能需要此功能。
参数
nextReducer
(Function) store使用的下一reducer
4.4 combineReducers(reducers)
随着应用变得越来越复杂,就需要将reducer函数拆分为单独的函数,每个函数管理状态(state)的独立部分。
combineReducers
辅助函数将不同的reducer值对象转换为可传递给createStore
的单个reducer函数。
生成的reducer调用每个子级的reducer,并将其结果收集到一个状态(state )对象中。由combinedReducers()
生成的state对象,会将每个reducer返回的state传给combinedReducers()
的,并以其key进行命名。
示例
rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer}) // This would produce the following state object { potato: { // ... potatoes, and other state managed by the potatoReducer ... }, tomato: { // ... tomatoes, and other state managed by the tomatoReducer, maybe some nice sauce? ... } }
通过对传入对象的reducer使用不同的key来控制返回state key的名称。如,可以通过调用combineReducers({ todos: myTodosReducer, counter: myCounterReducer })
来使结构为{ todos, counter }
。
常用的做法是,在reducer的管理的state后命名,因此可以使用ES6的简写形式:combineReducers({ counter, todos })
。这与combineReducers({ counter: counter, todos: todos })
是等价的。
参数
reducers
(Object):一个对象,其值对应于不同的reducer,之前会将值合并为一个。需要的所有reducer必须遵循的一些规则,请参见以下备注。
备注:早期文档建议使用ES6 import * reducers
语法来获取reducer对象。这造成了很多困惑,这也是为什么建议使用combineReducers()
从reducers/index.js
导入的原因。下面有一个示例。
返回值
(Function):一个调用reducer
对象中所有reducer的reducer,并构造一个结构相同的state。
注意:
该函数的较为主观,偏向于帮助初学者避免常见的陷阱。这也是为什么如果手工编写根reducer时会不必遵循相关规则的原因。
每个传入combineReducers
的reducer都需要遵守以下规则:
- 对于任何无法匹配的actoin,必须把所接收到的第一个参数
state
返回。 - 绝不能返回
undefined
。过早的return
时很容易犯此错误,因此,如果执行该操作,combineReducers
会抛出该错误,而不是让错误传到其它地方。 - 如果传入的
state
是undefined
,那么必须返回reducer的初始state。根据前面规则,初始state
也不能是undefined
。使用ES6可选参数语法来指定它很方便,但是也可以显式检查第一个参数是否是undefined
。
示例
reducers/todos.js
export default function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return state.concat([action.text]) default: return state } }
reducers/counter.js
export default function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1 case 'DECREMENT': return state - 1 default: return state } }
reducers/index.js
import { combineReducers } from 'redux' import todos from './todos' import counter from './counter' export default combineReducers({ todos, counter })
App.js
import { createStore } from 'redux' import reducer from './reducers/index' const store = createStore(reducer) console.log(store.getState()) // { // counter: 0, // todos: [] // } store.dispatch({ type: 'ADD_TODO', text: 'Use Redux' }) console.log(store.getState()) // { // counter: 0, // todos: [ 'Use Redux' ] // }
提示
- 这个函数只是一个辅助。可以编写自己的、以不同方式工作的
combineReducers
,甚至可以手动从子级reducer组装状态对象,并显式地编写根reducer函数,就像编写其它函数一样。 - 可以在reducer层次结构的任何级别上调用
combineReducers
。而不必发生在最根层。实际上,可以再次使用它来将过于复杂的子级reducer拆分为独立的更小层级的reducer,依此类推。
4.5 applyMiddleware(...middlewares)
建议使用中间件来扩展具有自定义功能的Redux。中间件使你可以包装store的dispatch
方法,以达到你想要的目的。中间件的关键特性是可组合。多个中间件可以组合在一起,其中每个中间件不需要了解链中前后的内容。
中间件最常见的用例是支持异步操作,而无需引入大量重复代码或使用Rx之类的依赖库。这样就可以像常规则action一样派发(dispatch)异步action。
例如,redux-thunk允许dispatch function,以让action creator控制反转。其接受dispatch作为参数,并且可以异步调用它。这样的功能称为thunk。中间件的另一个示例是redux-promise,它使可以dispatchPromise异步action,并在Promise resolve后dispatch一个普通action。
中间件没有引入createStore
中,也不是Redux架构的基本组成部分,但它十分有用,因此有必要可以在Redux核心中对它支持。这样,虽然中间件可能会在表达性和实用性上有所不同,但它被做为扩展dispatch的一种标准方法。
参数
...middleware
(arguments): 符合Redux中间件API的函数。每个中间件都将Store
的dispatch
和getState
函数作为命名参数接收,并返回一个函数。该函数将被传给next
中间件的调度方法,并且可能返回一个可能不同的参数,或在不同的时间或者根本不调用它的action
函数,而是调用next(action)
。链中的最后一个中间件将接收实际store的dispatch
方法作为next
参数,从而结束调用链。因此,中间件签名:({ getState, dispatch }) => next => action
返回值
(Function) 应用给定中间件的store 扩展器。store 扩展器签名是createStore => createStore
,但最简单的使用方法是将其作为最后一个enhancer
参数传递给createStore()
。
示例
自定义日志打印中间件:
import { createStore, applyMiddleware } from 'redux' import todos from './reducers' function logger({ getState }) { return next => action => { console.log('will dispatch', action) // 在中间件链中调用下一个要`dispatch`的方法`next(action)` const returnValue = next(action) console.log('state after dispatch', getState()) // 除非是中间件对其做进一步修改,否则这可能就是 action 本身 return returnValue } } const store = createStore(todos, ['Use Redux'], applyMiddleware(logger)) store.dispatch({ type: 'ADD_TODO', text: 'Understand the middleware' }) // (本行将由中间件打印:) // will dispatch: { type: 'ADD_TODO', text: 'Understand the middleware' } // state after dispatch: [ 'Use Redux', 'Understand the middleware' ]
在异步Action中使用thunk中间件:
import { createStore, combineReducers, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import * as reducers from './reducers' const reducer = combineReducers(reducers) // `applyMiddleware`通过中间件对`createStore`进一步修改: const store = createStore(reducer, applyMiddleware(thunk)) function fetchSecretSauce() { return fetch('https://www.google.com/search?q=secret+sauce') } // 这些是目前为止我们所看到的正常`action creator`。 // 它们返回的`action`可以在没有任何中间件的情况下进行调度(`dispatch`)。 // 但是,它们仅表示“事实”,而不表示“异步流程”。 function makeASandwich(forPerson, secretSauce) { return { type: 'MAKE_SANDWICH', forPerson, secretSauce } } function apologize(fromPerson, toPerson, error) { return { type: 'APOLOGIZE', fromPerson, toPerson, error } } function withdrawMoney(amount) { return { type: 'WITHDRAW', amount } } // 即使没有中间件,也可以调度`action`: store.dispatch(withdrawMoney(100)) // 但是,如果需要异步`action`时该怎么变呢? // 例如:API调用或路由转换 // 遇见 thunks // thunk是返回函数的函数 // 以下是一个: function makeASandwichWithSecretSauce(forPerson) { // 控制反转 // 返回一个接受`dispatch`的函数,以便我们稍后调度。 // Thunk中间件知道如何将thunk异步action转变为`action`。 return function(dispatch) { return fetchSecretSauce().then( sauce => dispatch(makeASandwich(forPerson, sauce)), error => dispatch(apologize('The Sandwich Shop', forPerson, error)) ) } } // Thunk中间件使我可以将thunk异步action调度action store.dispatch(makeASandwichWithSecretSauce('Me')) // 甚至从调度中返回thunk的返回值, // 这样只要我返回Promises,就可以将它们链接起来。 store.dispatch(makeASandwichWithSecretSauce('My wife')).then(() => { console.log('Done!') }) // 实际上,我可以编写 action creator, // 并从其它 action creator 调度action和异步action, // 并且可以使用Promises构建控制流 function makeSandwichesForEverybody() { return function(dispatch, getState) { if (!getState().sandwiches.isShopOpen) { // 你可以不返回Promises,但这是一个方便的约定, // 这样调用方可以始终在异步调度结果上调用`.then()`。 return Promise.resolve() } // 我们可以调度普通对象action和其它 thunk action, // 这使我们可以在单个流中组成异步action。 return dispatch(makeASandwichWithSecretSauce('My Grandma')) .then(() => Promise.all([ dispatch(makeASandwichWithSecretSauce('Me')), dispatch(makeASandwichWithSecretSauce('My wife')) ]) ) .then(() => dispatch(makeASandwichWithSecretSauce('Our kids'))) .then(() => dispatch( getState().myMoney > 42 ? withdrawMoney(42) : apologize('Me', 'The Sandwich Shop') ) ) } } // 这对于服务器端渲染非常有用, // 因为我可以等到数据可用后再同步渲染应用。 import { renderToString } from 'react-dom/server' store .dispatch(makeSandwichesForEverybody()) .then(() => response.send(renderToString(<MyApp store={store} />))) // 每当组件属性发生变化以加载丢失的数据时, // 还可以从该组件调度一个thunk 异步 action。 import { connect } from 'react-redux' import { Component } from 'react' class SandwichShop extends Component { componentDidMount() { this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } componentDidUpdate(prevProps) { if (prevProps.forPerson !== this.props.forPerson) { this.props.dispatch(makeASandwichWithSecretSauce(this.props.forPerson)) } } render() { return <p>{this.props.sandwiches.join('mustard')}</p> } } export default connect(state => ({ sandwiches: state.sandwiches }))(SandwichShop)
提示
- 中间件仅包装store的
dispatch
函数。从技术上讲,中间件可以做的任何事情,都可以通过包装每个dispatch
调用来手动完成,但是在一个地方进行管理并在整个项目的规模上定义action转换会更容易。 - 如果除了
applyMiddleware
之外还使用其它扩展器,但应确保在组合链中将applyMiddleware
放在它们之前,因为中间件可能是异步的。例如,它应该放在redux-devtools之前,如果不这样DevTools将看不到等Promise中间件发出的原始action。 - 如果要有条件地应用中间件,应确保仅在需要时才导入它:
let middleware = [a, b] if (process.env.NODE_ENV !== 'production') { const c = require('some-debug-middleware') const d = require('another-debug-middleware') middleware = [...middleware, c, d] } const store = createStore( reducer, preloadedState, applyMiddleware(...middleware) )
这使绑定工具更容易拆分出不需要的模块,并减小构建的大小。 - 那么
applyMiddleware
本身是什么?它应该是一种比中间件本身更强大的扩展机制。实际上applyMiddleware
被称为是Redux强大的扩展机制(store 扩展器) 的一个示例。 - 中间件听起来比实际要复杂。了解中间件的唯一方法是查看现有中间件的工作方式,然后尝试编写自己的中间件。函数嵌套可能会令人生畏,但是实际上您会发现大多数中间件都是只有10行代码而已,而嵌套和可组合性正是使中间件系统强大的原因。
- 要应用多个store 扩展器,可以使用
compose()
。
4.6 bindActionCreators(actionCreators, dispatch)
将值是action creator的对象转换为有相同key的对象,但每个action creator都包装在dispatch
调用中,以便可以直接调用。
通常,应该只在Store
实例上直接调用dispatch
。如果将Redux与React一起使用,则react-redux将会提供dispatch
功能,因此也可以直接调用。
bindActionCreators
的唯一用例是,当要将一些action creator传给一个不了解Redux组件,并且不想将dispatch
或Redux Store传递给该组件时。
为了方便,还可以将action creator作为第一个参数传递,并返回dispatch
包装函数。
参数
actionCreators
(Function 或 Object): 一个action creator,或者值是action creator的对象
返回值
(Function 或 Object):一个模拟原始对象的对象,但是每个函数会立即分发相应action creator返回的action。如果将一个函数作为actionCreators
传递,则返回值也是一个函数。
示例
TodoActionCreators.js
export function addTodo(text) { return { type: 'ADD_TODO', text } } export function removeTodo(id) { return { type: 'REMOVE_TODO', id } }
SomeComponent.js
import { Component } from 'react' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as TodoActionCreators from './TodoActionCreators' console.log(TodoActionCreators) // { // addTodo: Function, // removeTodo: Function // } class TodoListContainer extends Component { constructor(props) { super(props) const { dispatch } = props // Here's a good use case for bindActionCreators: // You want a child component to be completely unaware of Redux. // We create bound versions of these functions now so we can // pass them down to our child later. this.boundActionCreators = bindActionCreators(TodoActionCreators, dispatch) console.log(this.boundActionCreators) // { // addTodo: Function, // removeTodo: Function // } } componentDidMount() { // Injected by react-redux: let { dispatch } = this.props // Note: this won't work: // TodoActionCreators.addTodo('Use Redux') // You're just calling a function that creates an action. // You must dispatch the action, too! // This will work: let action = TodoActionCreators.addTodo('Use Redux') dispatch(action) } render() { // Injected by react-redux: let { todos } = this.props return <TodoList todos={todos} {...this.boundActionCreators} /> // An alternative to bindActionCreators is to pass // just the dispatch function down, but then your child component // needs to import action creators and know about them. // return <TodoList todos={todos} dispatch={dispatch} /> } } export default connect(state => ({ todos: state.todos }))(TodoListContainer)
提示
- 为什么Redux不像Flux那样立即将action creator绑定到store实例?这与需要在服务端上渲染的通用应用时,无法很好地配合使用有关。你很可能希望每个请求有一个单独的store实例,以便为它们准备不同的数据,但是在定义action时绑定action creator意味着对所有请求都只能使用一个store实例。
- 如果使用ES5,则可以将
require('./TodoActionCreators')
传给bindActionCreators
作为第一个参数,而不是import * as
语法。它唯一关心的是actionCreators
属性的值是函数,使用什么样的模块系统并不重要。
4.7 compose(...functions)
从右到左组合多个函数。
这是一个功能性的辅助函数,为方便起见包含在Redux中。在需要将多个store 扩展器组合起来时可能会用到。
参数
- (arguments):要组合的函数。每个函数应接受一个参数。其返回值将作为位于左侧的函数的参数传入,依此类推。最右边的函数参数例外,它可以接受多个参数,因为它将为生成的组合函数提供签名。
返回值
(Function):从右向左组合后的最终函数
示例
以下示例演示了如何使用compose
通过applyMiddleware
和redux-devtools包中的一些工具来增强store。
import { createStore, applyMiddleware, compose } from 'redux' import thunk from 'redux-thunk' import DevTools from './containers/DevTools' import reducer from '../reducers' const store = createStore( reducer, compose( applyMiddleware(thunk), DevTools.instrument() ) )
提示
- 所有
compose
所做的就是编写深层嵌套的函数转换,而无需修改代码