
本章及下一章将着眼于一种叫作Redux的数据架构。本章将讨论Redux背后的理念,建造一个自己的迷你版Redux并把它连接到Angular。在下一章中,我们将使用Redux构建一个更大的应用。
到目前为止,我们的大多数项目都在通过一种相当直接的方式管理状态:从服务中获取数据,然后在组件中渲染数据。在组件树中,值是沿着自上而下的方向传递的。
对于比较小的应用来说,这种管理方式已经足够了;但随着应用的成长,让多个组件来管理状态的不同部分将变得难以处理。比如,通过组件树向下传递所有值的方式有如下缺点。
●属性的间接传递:为了让任何组件都可以获取到应用的状态,我们不得不通过inputs属性向下传递值。这意味着我们会借助很多中间组件来传递状态,而这些中间组件既不使用也不关心传递的状态。
●重构不灵活:传递inputs属性时要贯穿整个组件树,从而导致父子组件之间产生耦合,而这些耦合通常都是不必要的。这样,试图把一个子组件放入组件树的其他层级中会变得非常困难,因为我们必须修改所有新的父级组件来传递状态。
●状态树和DOM树不匹配:状态的“形状”往往和视图/组件层级的“形状”不匹配。当我们需要引用组件树一个较远分支中的数据时,通过组件树的属性来传递所有值就会使我们陷入困境。
●应用中到处都是状态:如果通过组件来管理状态,就很难获取应用整体状态的快照。因此很难知道哪个组件“拥有”一条特定的数据以及哪些组件关心该数据的变化。
把数据从组件中提取出来并放到服务中会有很大的帮助。至少,如果服务是数据的“拥有者”,那么对于把数据放在哪里,我们就有更清晰的概念。但这也带来了一个新问题:关于“让服务拥有数据”的最佳实践又是什么呢?有什么可以遵循的模式吗?当然有!
本章会讨论一种叫作Redux的数据架构模式,其设计初衷就是要解决这些问题。我们将自己实现一个Redux,它会把所有的状态都存储在一个地方。这种“把所有应用状态都存在同一个地方”的想法乍听起来可能有点疯狂,但最终会给你惊喜。
如果你还没听说过Redux,可以到其官网http://redux.js.org/查看相关内容。网络应用的数据架构一直在进化,搭建数据架构的传统方式已经不能很好地适应大型网络应用。因为功能强大且易于理解,Redux如今非常流行。
数据架构是一个复杂的话题,而Redux的最大优点可能是它的简单性。如果把Redux剥离得只剩核心代码,其代码行数将不到100行。
通过把Redux用作应用的骨架,我们可以构建出更容易理解的富网络应用。首先,我们来看看如何编写一个迷你版Redux,稍后再把这些概念应用到一个更大的应用程序中,以更好地理解Redux的工作模式。

有人尝试使用Redux或新建一个受Redux启发的、能与Angular协同工作的系统。以下是两个著名的例子:
ngrx是一个受Redux启发的架构,也是可观察对象的重度使用者。angular2-redux则依赖于Redux并添加了一些Angular的辅助类(依赖注入、可观察对象包装)。
这里不会使用它们。为了在不引入新依赖的前提下更好地展示概念,我们将直接使用Redux。当然,在你编写自己的应用时,这两个类库可能会对你有所帮助。
Redux:核心概念
Redux的核心概念有:
●应用的所有数据都放在一个叫作state的数据结构之中,而state存放在store中;
●应用从store中读取state;
●store永远不会被直接修改;
●action描述发生了什么,由用户交互(和其他代码)触发;
●通过调用一个叫作reducer的函数来结合旧的state和action会创建出新的state(如图12-1所示)。

图12-1 Redux的内核
如果以上几点还不够清楚的话,也不用担心。本章的其余部分会把这些概念应用到实践中。
12.2.1 reducer是什么
我们先来讨论reducer(归集器)。reducer的概念是:接收旧的state和action并返回新的state。
(1)它不能直接修改当前的state;
(2)它不会使用参数之外的任何数据。
换句话说,一个纯函数在参数不变的情况下,总是会返回同一个值;而且纯函数不会调用任何会对外界产生影响的函数。比如,没有数据库调用,没有HTTP请求,也不会改变外部的数据结构。
reducer应始终把当前state当作只读的。reducer不应该改变state,而是应该返回一个新的state。(通常,新的state会从复制原有state开始,但我们不应该自己动手复制它。)
下面来定义我们的第一个reducer。记住,reducer涉及以下三点。
(1)Action:定义要做什么(能带可选参数)。
(2)state:存储应用中的所有数据。
(3)Reducer:接收state和Action并返回一个新的state。
12.2.2 定义Action和Reducer的接口
因为我们使用TypeScript是为了确保全程都是带类型的,所以先为Action和Reducer设计一套接口。
●Action接口
Action接口如下所示。
code/redux/angular2-redux-chat/minimal/tutorial/01-identity-reducer.ts
interface Action {
type:string;
payload?:any;
}
注意Action有两个字段:
(1)type
(2)payload
type是一个标识字符串,用来描述action的类型,比如INCREMENT或ADD_USER。payload可以是任意类型的对象。payload?中的?表示这个字段是可选的。
●Reducer接口
Reducer接口如下所示。
code/redux/angular2-redux-chat/minimal/tutorial/01-identity-reducer.ts
interface Reducer<T> {
(state:T, action:Action):T;
}
Reducer使用了TypeScript中一种名叫泛型的特性。在这个例子中,T就是state的类型。注意,这里我们要表达的是:有效的Reducer就是一个函数,它接收state(类型为T)和action并返回一个新的state(类型也是T)。
12.2.3 创建第一个Reducer
最简单的reducer返回state本身。(可以把它叫作identity reducer,因为它在state上应用了“identity函数”
。这也是所有reducer的默认情况,我们很快就会看到。)
code/redux/angular2-redux-chat/minimal/tutorial/01-identity-reducer.ts
let reducer:Reducer<number> =(state:number, action:Action)=> {
return state;
};
注意,这个Reducer通过语法Reducer<number>把泛型中的类型固定为number。我们很快就会定义一些比数字更复杂的state。
我们还没有使用Action,但已经可以试用这个Reducer了。

运行本节的示例
你可以在code/redux文件夹中找到本章的代码。如果示例是可运行的,那么你就会在代码块上方看到文件名。
在本节中,这些例子是在浏览器之外通过node.js来运行的。因为这些例子中用的是TypeScript,所以你应该使用命令行工具ts-node(而不是直接使用node)来运行它们。
可以运行下面的命令来安装ts-node:
npm install -g ts-node
也可以在code/redux/angular2-redux-chat目录下运行npm install,然后调用./node_modules/.bin/ts-node ——noProject。
比如,要运行上面的例子,你需要输入下列命令(不要输入$符):
$ cd code/redux/angular2-redux-chat/minimal/tutorial
$ ../../node_modules/.bin/ts-node ——noProject 01-identity-reducer.ts
在我们告诉你把运行环境切换到浏览器之前,本章其余的代码也都用同样的步骤运行。
12.2.4 运行第一个Reducer
把所有代码整合起来并运行这个reducer。
code/redux/angular2-redux-chat/minimal/tutorial/01-identity-reducer.ts
interface Action {
type:string;
payload?:any;
}
interface Reducer<T> {
(state:T, action:Action):T;
}
let reducer:Reducer<number> =(state:number, action:Action)=> {
return state;
};
console.log(reducer(0, null)); // -> 0
运行下列命令:
$ cd code/redux/angular2-redux-chat/minimal/tutorial
$ ../../node_modules/.bin/ts-node ——noProject 01-identity-reducer.ts
0
用这段代码作为示例似乎有点傻,但它教给了我们reducer的第一条原则:
默认情况下,reducer返回state本身。
在这个例子中,我们传入了一个值为数字0的state和一个值为null的action。reducer返回的结果是值为数字0的state。
但是我们还要做一些更有趣的事来改变state。
12.2.5 使用action调整计数器
我们最终的state会远比一个数字复杂得多。我们会把应用中的所有数据都保存在state中,这就需要为最终的state设计一种更好的数据结构。
不过,目前使用一个数字作为state可以让我们专注于其他问题。因此我们先沿用这种做法,state仅仅是一个用来存储计数器的数字。
假设我们希望改变state的数值。记住,我们不会在Redux中修改state。取而代之的是创建action,用来告诉reducer如何生成一个新的state。
让我们创建一个Action来改变计数器。要记住,Action唯一的必选属性就是type。我们可以像这样来定义第一个action:
let incrementAction:Action = { type:'INCREMENT' }
我们还应该创建第二个action,它负责通知reducer让计数器变小:
let decrementAction:Action = { type:'DECREMENT' }
现在有了这些action,我们来试试在reducer中使用它们。
code/redux/angular2-redux-chat/minimal/tutorial/02-adjusting-reducer.ts
let reducer:Reducer<number> =(state:number, action:Action)=> {
if(action.type === 'INCREMENT'){
return state + 1;
}
if(action.type === 'DECREMENT'){
return state - 1;
}
return state;
};
现在可以试用完整的reducer了。
code/redux/angular2-redux-chat/minimal/tutorial/02-adjusting-reducer.ts
let incrementAction:Action = { type:'INCREMENT' };
console.log(reducer(0, incrementAction)); // -> 1
console.log(reducer(1, incrementAction)); // -> 2
let decrementAction:Action = { type:'DECREMENT' };
console.log(reducer(100, decrementAction)); // -> 99
漂亮!现在会根据传给reducer的action来决定返回的新state的值。
12.2.6 reducer的switch
我们通常把reducer的主体代码换成switch语句,而不是一大堆if。
code/redux/angular2-redux-chat/minimal/tutorial/03-adjusting-reducer-switch.ts
let reducer:Reducer<number> =(state:number, action:Action)=> {
switch(action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state; // <—— dont forget!
}
};
let incrementAction:Action = { type:'INCREMENT' };
console.log(reducer(0, incrementAction)); // -> 1
console.log(reducer(1, incrementAction)); // -> 2
let decrementAction:Action = { type:'DECREMENT' };
console.log(reducer(100, decrementAction)); // -> 99
// any other action just returns the input state
let unknownAction:Action = { type:'UNKNOWN' };
console.log(reducer(100, unknownAction)); // -> 100
注意switch语句的default分支要返回state本身。当传入一个未知的action时,这将确保程序不会报错而且我们能得到原始的state值。

问:等一下!难道要把应用中所有的state都放在一个庞大的switch语句中吗?
答:既是又不是。
如果这是你第一次接触Redux的reducer,那么“对应用中state的所有更改都是一个庞大switch语句的结果”可能会让你感到奇怪。你应该知道下面两点。
(1)在一个地方集中管理state的变化对于维护程序有莫大的帮助,具体来说是因为当把所有状态都集中在一起时就很容易查出哪里发生了变化。(此外,你可以轻松地定位state的变化是哪个action的结果,因为你可以把action的type属性作为关键字在代码中进行搜索。)
(2)你可以(而且经常会)将reduer分解成若干sub-reducer(子reducer),它们各自负责管理state树中的一个不同分支。我们稍后会进行讨论。
12.2.7 action的“参数”
在上个例子中,我们的action只包含一个type属性,用来告诉reducer是递增还是递减这个state。
然而,应用的变化通常是无法通过单一的值来描述清楚的,而是需要一些参数来描述这种变化。这就是在Action里有payload字段的原因了。
在这个计数器示例中,如果我们想要让计数器增加9。一种做法是发送9次INCREMENT action,但这样做效率太低,尤其是在想增加一个较大数值的时候,如9000。
替代方案是增加一个PLUS action。它用payload参数来发送一个数字,这个数字表示计数器要增加的值。定义这个action很简单:
let plusSevenAction = { type:'PLUS', payload:7 };
接下来,要支持这个action,就要在reducer里添加一个新的case分支来处理PLUS action。
code/redux/angular2-redux-chat/minimal/tutorial/04-plus-action.ts
let reducer:Reducer<number> =(state:number, action:Action)=> {
switch(action.type){
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
case 'PLUS':
return state + action.payload;
default:
return state;
}
};
PLUS分支会把action.payload中的任何数字累加到state上。下面来试试看。
code/redux/angular2-redux-chat/minimal/tutorial/04-plus-action.ts
console.log(reducer(3, { type:'PLUS', payload:7})); // -> 10
console.log(reducer(3, { type:'PLUS', payload:9000})); // -> 9003
console.log(reducer(3, { type:'PLUS', payload:-2})); // -> 1
我们在第一行接收的state是3,然后加上7,得到的结果是10。漂亮!不过,请注意当我们传递state的时候,它并没有真的发生变化。也就是说,我们没有保存reducer变化产生的结果,不能在之后的action中复用它。
这些reducer都是纯函数,不会改变外部环境。问题在于,应用中的一切都在不断变化。特别是在state变化后,我们必须在某个地方保留这个新的state。
在Redux中,state是保存在store里的。store负责运行reducer然后保存新的state。我们来看一个最简单的store。
code/redux/angular2-redux-chat/minimal/tutorial/05-minimal-store.ts
class Store<T> {
private _state:T;
constructor(
private reducer:Reducer<T>,
initialState:T
){
this._state = initialState;
}
getState():T {
return this._state;
}
dispatch(action:Action):void {
this._state = this.reducer(this._state, action);
}
}
注意Store是泛型的,我们指定state的类型为泛型T,并用私有变量_state来存储state。
Store还应该有一个Reducer,它同样是泛型的,泛型的类型是T。这是因为每个store都和一个特定的reducer紧密相关。我们用私有变量reducer来存储这个Reducer。
在Redux中,每个应用通常只有一个store和一个顶层reducer。
让我们来仔细看看State中的每个方法:
●在构造函数中把_state变量设置为初始的state;
●getState()直接返回当前的_state变量;
●dispatch接收一个action并把它传给reducer,然后用返回值来更新_state变量的值**。
注意dispatch方法不返回任何值。它只更新store中的state(结果返回之后)。这是Redux中的一条重要原则:分发(dispatch)action是一种“触发并忘记”的策略。分发action并不直接操作state,所以它也不返回新的state。
当我们分发action的时候,会发送一个关于发生了什么的通知。如果想要了解系统的当前状态,就必须检查store中的state。
12.3.1 使用store
我们来试试store。
code/redux/angular2-redux-chat/minimal/tutorial/05-minimal-store.ts
// create a new store
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // -> 0
store.dispatch({ type:'INCREMENT' });
console.log(store.getState()); // -> 1
store.dispatch({ type:'INCREMENT' });
console.log(store.getState()); // -> 2
store.dispatch({ type:'DECREMENT' });
console.log(store.getState()); // -> 1
先创建一个新的Store对象并保存在store变量中。我们可以使用这个变量来获取当前的state并且分发action。
state初始值为0,然后进行两次INCREMENT、一次DECREMENT,最终的state值是1。
12.3.2 使用subscribe进行通知
Store记录着发生的变化,这很不错;但是在上个示例中,我们必须用store.getState()询问state的变化。如果一个新的action分发后能立刻让我们知道就好了,这样我们就能作出响应了。要做到这一点,可以实现观察者模式(observer pattern)。也就是说,我们会注册一个回调函数用来订阅所有的变化。
我们希望它这样工作:
(1)我们用subscribe注册一个监听函数;
(2)当dispatch被调用时,我们遍历所有的监听器并逐个调用它们,它们会负责通知大家这个state发生了变化。
●注册监听器
监听回调函数是没有参数的函数。我们来定义一个接口,以便于描述。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
interface ListenerCallback {
():void;
}
订阅一个监听器后,我们可能还需要取消订阅,因此也为取消订阅函数定义个接口。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
interface UnsubscribeCallback {
():void;
}
这段代码没什么内容,它只是另一个没有参数的函数,也没有返回值。但定义这些类型能让我们的代码更容易阅读。
store还要保存一个ListenerCallbacks的列表。我们把这个列表加到Store中。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
class Store<T> {
private _state:T;
private _listeners:ListenerCallback[] = [];
接着我们就可以用subscribe函数把监听器添加到_listeners列表中了。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
subscribe(listener:ListenerCallback):UnsubscribeCallback {
this._listeners.push(listener);
return()=> { // returns an "unsubscribe" function
this._listeners = this._listeners.filter(l => l!== listener);
};
}
subscribe接收一个ListenerCallback参数(也就是一个没有参数、没有返回值的函数)并返回UnsubscribeCallback(方法签名同上)。添加监听器很简单:只要用push方法把它追加到_listeners数组中就可以了。
它的返回值是一个函数。这个函数会修改_listeners列表,把刚加上的listener过滤掉。也就是说,它返回UnsubscribeCallback函数,调用此函数就会把刚加上的listener从列表中移除。
●通知监听器
每当state发生变化时,我们都要调用这些监听函数。也就是说,无论是分发了一个新的action还是state发生变化,我们都要调用所有监听器。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
dispatch(action:Action):void {
this._state = this.reducer(this._state, action);
this._listeners.forEach((listener:ListenerCallback)=> listener());
}
●完整的store
稍后我们会亲自尝试这个store,不过现在先看看Store最新的完整代码清单。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
class Store<T> {
private _state:T;
private _listeners:ListenerCallback[] = [];
constructor(
private reducer:Reducer<T>,
initialState:T
){
this._state = initialState;
}
getState():T {
return this._state;
}
dispatch(action:Action):void {
this._state = this.reducer(this._state, action);
this._listeners.forEach((listener:ListenerCallback)=> listener());
}
subscribe(listener:ListenerCallback):UnsubscribeCallback {
this._listeners.push(listener);
return()=> { // returns an "unsubscribe" function
this._listeners = this._listeners.filter(l => l!== listener);
};
}
}
●试用subscribe
现在可以订阅这个store的变化了,试试看。
code/redux/angular2-redux-chat/minimal/tutorial/06-store-w-subscribe.ts
let store = new Store<number>(reducer, 0);
console.log(store.getState()); // -> 0
// subscribe
let unsubscribe = store.subscribe(()=> {
console.log('subscribed:', store.getState());
});
store.dispatch({ type:'INCREMENT' }); // -> subscribed:1
store.dispatch({ type:'INCREMENT' }); // -> subscribed:2
unsubscribe();
store.dispatch({ type:'DECREMENT' }); //(nothing logged)
// decrement happened, even though we weren't listening for it
console.log(store.getState()); // -> 1
我们订阅了store并在其回调函数中输出日志subscribed:以及store的当前state。
注意,监听函数并没有把当前state作为参数传进来。尽管这个选择看起来有点奇怪,但这是因为还有另一些细节需要权衡。现在把state的变更通知和当前state分开会更利于思考。在此就不再深入探究了,要了解更多信息,请阅读Mhttps://github.com/reactjs/redux/issues/1707、https://github.com/reactjs/redux/issues/1513和https://github.com/reactjs/redux/issues/303。
我们保存了unsubscribe回调函数。接下来要注意,在调用unsubscribe()之后就不会再输出日志了。我们仍然可以分发action,但却看不到它的结果了,除非直接向store询问。

如果喜欢RxJS和可观察对象,你可能会想到,其实也可以用RxJS实现自己的订阅监听器。你可以重写Store,用可观察对象代替我们自行实现的订阅机制。
英雄所见略同。事实上,我们已经替你做好了,你可以在文件code/redux/angular2-redux-chat/minimal/tutorial/06b-rx-store.ts中找到示例代码。
如果你愿意使用RxJS作为应用的数据骨架,那么用RxJS实现Store就是一种有趣而强大的模式。
我们在这里并没有过多使用可观察对象,主要是因为我们想讨论Redux本身以及如何使用一个单独的state树来思考数据架构。Redux本身已经强大到不必借助可观察对象就可以在应用中使用了。
一旦你领悟了如何使用“正统”Redux,那么再加入可观察对象就一点也不难了(前提是你已经理解了RxJS)。我们先暂且使用“正统”Redux,本章结尾处会给出一些指引,告诉你如何使用基于可观察对象的Redux包装器。
12.3.3 Redux核心
上面这个store就是Redux的基本内核。reducer接收当前state和action并返回一个新的state,这个state会保存在store中。
想要构建一个用于生产环境的大型网络应用,我们显然还要添加更多。但是,我们稍后涉及的所有新概念都将以这样一个简单的概念为基础:state是不可改变的,是集中存储的。如果掌握了之前提到的这些概念,也可以发明一些能用在高级Redxu应用中的模式(以及类库)。
在Redux的日常使用过程中,还有许多我们未曾涉及的方面。比如,我们需要知道:
●如何在state中精心处理更复杂的数据结构;
●当state发生变化时,如何不必轮询state就得到通知(使用订阅);
●如何拦截分发进行调试(也叫middleware);
●如何计算派生值(使用选择器);
●如何把一个大型reducer分解成许多可维护的小型reducer(并重新组合);
●如何处理异步数据。
我们将在本章的剩余部分和下一章中逐一解释这些问题并讲解常用的模式。
我们首先介绍如何在state中处理更复杂的数据结构。为此,我们需要一个比计数器更有意思的示例。那就构建一个聊天应用吧,用户可以用它向彼此发送消息。
在这个聊天应用中(以及所有Redux应用中)数据模型有三个主要部分:
(1)state
(2)action
(3)reducer
12.4.1 消息应用的state
计数器应用中的state只是一个数字,而在这个消息应用中,state是一个对象。
这个state对象只有一个属性messages。messages是一个字符串数组,每个字符串表示应用中的一条消息。例如:
// an example `state` value
{
messages:[
'here is message one',
'here is message two'
]
}
我们可以这样定义该应用中的state类型。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
interface AppState {
messages:string[];
}
12.4.2 消息应用的action
这个应用将处理两个action:ADD_MESSAGE和DELETE_MESSAGE。
action对象ADD_MESSAGE永远都有一个属性message,这个属性表示添加到state中的消息。action对象ADD_MESSAGE的模型如下:
{
type:'ADD_MESSAGE',
message:'Whatever message we want here'
}
action对象DELETE_MESSAGE会从state中删除一条指定的消息。这里的问题在于,我们要指出想删除的是哪条消息。
如果消息的数据结构是对象的话,可以在每条消息创建的时候赋予它一个id属性。然而,为了让这个示例尽可能简单,消息只是单纯的字符串,因此我们只能用另一种方式来删除消息了。目前最简单的方式就是直接使用消息数组里的索引(可以看作事实性的ID)。
明白这一点之后,action对象DELETE_MESSAGE的模型如下:
{
type:'DELETE_MESSAGE',
index:2 // <- or whatever index is appropriate
}
我们可以用TypeScript的语法interface …… extends来定义这些action的类型。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
interface AddMessageAction extends Action {
message:string;
}
interface DeleteMessageAction extends Action {
index:number;
}
这样AddMessageAction就能指定一条消息了,而DeleteMessageAction也可以指定一个索引。
12.4.3 消息应用的reducer
记住reducer需要处理两个action:ADD_MESSAGE和DELETE_MESSAGE。下面来分别讨论它们:
●处理ADD_MESSAGE
首先针对action.type使用switch语句并处理ADD_MESSAGE分支。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
let reducer:Reducer<AppState> =
(state:AppState, action:Action):AppState => {
switch(action.type){
case 'ADD_MESSAGE':
return {
messages:state.messages.concat(
(<AddMessageAction>action).message
),
};

TypeScript的对象本身已经有类型了,为什么还要添加一个type字段呢?
要处理这种“多态分发”(polymorphic dispatch),有很多方式可供选择。想区分不同类型的action并在同一个reducer里处理它们,一种非常简明的方式是在type字段里存一个字符串(这里type的意思是“action的类型”)。从某种程度上说,你确实不必为每个action创建一个新的接口。
不过,用反射来实现对具体类型的switch会更令人满意。虽然类型守卫
开启了这种可能性,但当前版本的TypeScript还做不到这一点。
从广义上来说,类型只是一个编译阶段的概念。代码编译成JavaScript后,会丢失一些类型的元数据。
当然,如果你觉得对type字段进行switch很麻烦,希望直接使用语言特性来实现的话,也可以使用“装饰器反射元数据”技术
。目前,用一个简单的type字段就足够了。
●添加一项而不改变原有数据
当处理ADD_MESSAGE时,我们需要把给定的消息添加到state中。像所有的reducer一样,我们需要返回一个新的state。要记住,reducer必须是纯函数并且不会改变旧的state。
下面的代码有什么问题?
case 'ADD_MESSAGE':
state.messages.push(action.message);
return { messages:messages };
// ……
问题在于这段代码改变了state.messages数组,也就是改变了旧的state。正确的做法是创建一个state.messages数组的副本并把新消息添加到这个副本中。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
case 'ADD_MESSAGE':
return {
messages:state.messages.concat(
(<AddMessageAction>action).message
),
};

语法<AddMessageAction>action会把action转换成更具体的类型。也就是说,reducer接收的是更通用的类型Action,它并没有messsage字段。如果这里我们没有进行转换,那么编译器就会报告说Action没有messsage字段。
但是,我们确实知道有一个ADD_MESSAGE action,所以就把它转化成一个AddMessageAction。使用圆括号来确保编译器知道我们要转化的是action而不是action.message。
记住,reducer必须返回一个新的AppState。当我们从reducer返回一个对象的时候,它必须匹配AppState的格式。在这个例子中,我们只需要一个关键字段messages;但在更复杂的state中,就要考虑更多字段了。
●删除一项而不改变原有数据
记住,当处理DELETE_MESSAGE action时,我们传入数组中消息的索引作为代理ID(另一种常见的做法是传入一个真实条目的ID)。另外,因为我们不想改变旧的messages数组,所以需要小心处理。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
case 'DELETE_MESSAGE':
let idx =(<DeleteMessageAction>action).index;
return {
messages:[
……state.messages.slice(0, idx),
……state.messages.slice(idx + 1, state.messages.length)
]
这里使用了两次slice操作符。首先获取要删除条目之前的所有条目,然后连接上其后的所有条目。

有4种不改变原有数据的常见操作:
●往数组中添加一项;
●从数组中移除一项;
●添加或修改对象中的键;
●从对象中移除键。
前两个(数组的)操作我们已经介绍过了。接下来我们将讨论更多关于对象的操作。目前需要知道的是一种使用Object.assign的常用方法,如下所示:
Object.assign({}, oldObject, newObject)
// <——————<————————————
你可以认为Object.assign方法是从右至左地合并对象。newObject合并到oldObject,再合并到{}。这样,oldObject的所有字段都会保留,除非字段在newObject中也存在。无论是oldObject还是newObject都不会被改变。
当然,进行这些处理时要小心谨慎,因为很容易犯错。这也是很多人使用Immutable.js
的一个原因,Immutable.js是一组有助于加强不变性的数据结构。
12.4.4 试用action
现在来尝试运行action。
code/redux/angular2-redux-chat/minimal/tutorial/07-messages-reducer.ts
let store = new Store<AppState>(reducer, { messages:[] });
console.log(store.getState()); // -> { messages:[] }
store.dispatch({
type:'ADD_MESSAGE',
message:'Would you say the fringe was made of silk?'
} as AddMessageAction);
store.dispatch({
type:'ADD_MESSAGE',
message:'Wouldnt have no other kind but silk'
} as AddMessageAction);
store.dispatch({
type:'ADD_MESSAGE',
message:'Has it really got a team of snow white horses?'
} as AddMessageAction);
console.log(store.getState());
// ->
// { messages:
// [ 'Would you say the fringe was made of silk?',
// 'Wouldnt have no other kind but silk',
// 'Has it really got a team of snow white horses?' ] }
我们先创建了一个新的store,然后调用store.getState(),从而看到一个空的messages数组。
接下来,我们往store中添加三条消息
。对于每条消息,我们都把type设为ADD_MESSAGE并把每个对象转换成AddMessageAction。
最后,我们把新的state打印出来,就能看到messages数组包含了所有这三条消息。
这三个dispatch语句都不够优雅,原因有以下两点。
(1)每次都需要手动指定type字符串。我们也可以改用常量,但是如果什么都不用做就更好了。
(2)需要手动转换成AddMessageAction。
我们应该创建一个函数来创建这些对象,而不是直接创建。编写函数来创建action的思想在Redux中很常见,因此这种模式有个名字:action creator。
12.4.5 action creator
我们要创建一个函数来创建ADD_MESSAGE action,而不是直接使用对象。
code/redux/angular2-redux-chat/minimal/tutorial/08-action-creators.ts
class MessageActions {
static addMessage(message:string):AddMessageAction {
return {
type:'ADD_MESSAGE',
message:message
};
}
static deleteMessage(index:number):DeleteMessageAction {
return {
type:'DELETE_MESSAGE',
index:index
};
}
}
这里创建了一个类,它有两个静态方法addMessage和deleteMessage,分别返回AddMessageAction和DeleteMessageAction。
你不一定要用静态方法作为action
creator,也可以使用普通的函数,命名空间中的函数,甚至是一个对象的实例方法等。关键是要用统一的方式来组织它们,让它们便于使用。
现在我们就改用新的action creator了。
code/redux/angular2-redux-chat/minimal/tutorial/08-action-creators.ts
let store = new Store<AppState>(reducer, { messages:[] });
console.log(store.getState()); // -> { messages:[] }
store.dispatch(
MessageActions.addMessage('Would you say the fringe was made of silk?'));
store.dispatch(
MessageActions.addMessage('Wouldnt have no other kind but silk'));
store.dispatch(
MessageActions.addMessage('Has it really got a team of snow white horses?'));
console.log(store.getState());
// ->
// { messages:
// [ 'Would you say the fringe was made of silk?',
// 'Wouldnt have no other kind but silk',
// 'Has it really got a team of snow white horses?' ] }
这样感觉好多了!
它还有一个额外的好处:如果最终决定要改变消息的格式,我们不用更新任何一处dispatch语句。比如,假设我们要给每条消息增加创建时间,就可以在addMessage方法中添加一个created_at字段,那么现在所有的AddMessageActions都会有created_at字段:
class MessageActions {
static addMessage(message:string):AddMessageAction {
return {
type:'ADD_MESSAGE',
message:message,
// something like this
created_at:new Date()
};
}
// ……
12.4.6 使用真正的Redux
现在我们已经写好了自己的迷你版Redux。你可能会问:“要想使用真正的Redux还需要做什么?”谢天谢地,没有多少要做的。让我们更新一下代码,现在就改用真正的Redux。
如果你还没准备好,那就需要在code/redux/angular2-redux-chat/minimal/tutorial目录下运行命令npm install。
首先要做的是从redux包中导入Action、Reducer和Store。同时还导入了一个辅助函数createStore。
code/redux/angular2-redux-chat/minimal/tutorial/09-real-redux.ts
import {
Action,
Reducer,
Store,
createStore
} from 'redux';
接下来,让reducer创建初始的state,而不是在创建store的时候指定。这里,我们让reducer的默认参数来做这件事。采用这种方式,如果没有state传入(例如在初始化阶段中reducer被首次调用)就会使用初始的state。
code/redux/angular2-redux-chat/minimal/tutorial/09-real-redux.ts
let initialState:AppState = { messages:[] };
let reducer:Reducer<AppState> =
(state:AppState = initialState, action:Action)=> {
reducer的其余部分都不用动,干得漂亮!
最后要做的是使用Redux的辅助函数createStore来创建store。
code/redux/angular2-redux-chat/minimal/tutorial/09-real-redux.ts
let store:Store<AppState> = createStore<AppState>(reducer);
之后一切正常!
code/redux/angular2-redux-chat/minimal/tutorial/09-real-redux.ts
let store:Store<AppState> = createStore<AppState>(reducer);
console.log(store.getState()); // -> { messages:[] }
store.dispatch(
MessageActions.addMessage('Would you say the fringe was made of silk?'));
store.dispatch(
MessageActions.addMessage('Wouldnt have no other kind but silk'));
store.dispatch(
MessageActions.addMessage('Has it really got a team of snow white horses?'));
console.log(store.getState());
// ->
// { messages:
// [ 'Would you say the fringe was made of silk?',
// 'Wouldnt have no other kind but silk',
// 'Has it really got a team of snow white horses?' ] }
现在我们只是单纯地使用Redux来解决问题,下一步还要把Redux和我们的网络应用联系起来。开始行动吧。
在上一节中,我们学习了Redux的核心并展示了如何在Redux中创建reducer以及使用store管理数据。现在我们要更进一步,把Redux和Angular组件结合起来。
我们将在本节中创建一个最小化的Angular应用。该应用只有一个计数器,可以通过按钮来增加或减少计数(如图12-2所示)。

图12-2 计数器应用
这种小应用可以让我们专注于Redux和Angular之间的集成点。在下一节中,我们将进一步讨论更大的应用。目前,我们先来看看如何构建这个计数器应用!

我们没有在Redux和Angular之间使用任何辅助类库,而是直接集成它们。其实有很多开源类库可以简化这一过程,参见12.13节。
不过,一旦你理解了其背后的原理,使用这些类库也会容易得多。这里我们所做的一切都是为了让你更好地理解Redux背后的原理。
你应该还记得,规划Redux应用的三个步骤是:
(1)定义应用中央state的数据结构;
(2)定义用来改变state的action;
(3)定义一个reducer,用于接收旧的state和一个action并返回新的state。
对于这个应用来说,我们只是要增加或者减少计数。这个功能已经在上一节实现了,所以你会对本节的action、store和reducer感到非常熟悉。
我们要做的另外一件事就是,在编写Angular应用时决定在哪里创建组件。在这个应用中,有一个顶层组件CounterApp,它包含一个CounterComponent组件。CounterComponent组件则包含屏幕截图所示的那个视图。
大致上,我们要做以下几件事:
(1)创建Store并通过依赖注入使它可以在整个应用中被访问到;
(2)订阅Store的变化并在组件中显示出来;
(3)当发生某些变化时(例如按下按钮时),我们将通过Store来分发一个action。
计划得差不多了,下面来看看如何在实践中应用!
首先导入一些稍后要用到的东西。
code/redux/angular2-redux-chat/minimal/app.ts
import {
Component
} from '@angular/core';
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import {
createStore,
Store,
StoreEnhancer
} from 'redux';
import { counterReducer } from './counter-reducer';
我们导入了Store(类)和createStore(辅助函数),之前用到过它们。我们还导入了一个叫作StoreEnhancer的新类,很快就会讲到它。
我们还从counter-reducer.ts中导入了reducer,从app-state.ts中导入了state的接口AppState。
12.7.1 定义应用的state
让我们来看看AppState。
code/redux/angular2-redux-chat/minimal/app-state.ts
export interface AppState {
counter:number;
};
这里把中央state的结构定义成了AppState,它是一个对象并且只有一个键counter(类型为number)。在下个示例(聊天应用)中,我们将讨论如何使用更复杂的state,但目前这样就足够了。
12.7.2 定义reducer
接下来定义reducer,它负责处理应用state中计数器的增加和减少。
code/redux/angular2-redux-chat/minimal/counter-reducer.ts
mport {
INCREMENT,
DECREMENT
} from './counter-action-creators';
let initialState:AppState = { counter:0 };
// Create our reducer that will handle changes to the state
export const counterReducer:Reducer<AppState> =
(state:AppState = initialState, action:Action):AppState => {
switch(action.type){
case INCREMENT:
return Object.assign({}, state, { counter:state.counter + 1 });
case DECREMENT:
return Object.assign({}, state, { counter:state.counter - 1 });
default:
return state;
}
};
我们先导入了两个常量INCREMENT和DECREMENT,它们是由action creator导出的。虽然它们只是被简单地定义成了字符串'INCREMENT'和'DECREMENT',但不错的是我们可以从编译器那里获得额外的帮助,以防打错字。我们稍后再来看看这些action creator。
initialState是一个AppState,它的counter属性为0。
counterReducer处理两个action:使当前计数器加1的INCREMENT以及使计数器减1的DECREMENT。这两个action都使用Object.assign来确保不会改变旧的state,而是创建一个新对象并把它作为新的state返回。
既然说到了这里,我们就来看看action creator。
12.7.3 定义action creator
action creator是函数,返回的是定义action的对象。下面的increment和decrement函数会返回一个定义了合适type的对象。
code/redux/angular2-redux-chat/minimal/counter-action-creators.ts
import {
Action,
ActionCreator
} from 'redux';
export const INCREMENT:string = 'INCREMENT';
export const increment:ActionCreator<Action> =()=>({
type:INCREMENT
});
export const DECREMENT:string = 'DECREMENT';
export const decrement:ActionCreator<Action> =()=>({
type:DECREMENT
});
注意,action creator函数返回的是类型ActionCreator<Action>。ActionCreator是一个Redux定义的泛型类,可以用来定义action的创建函数。在这个例子中,我们使用的具体类是Action,但也可以使用一个更具体的类,比如上一节定义的AddMessageAction。
12.7.4 创建store
现在有了reducer和state,我们可以这样创建store。
let store:Store<AppState> = createStore<AppState>(counterReducer);
不过,Redux有一点非常棒,那就是它有一组健壮的开发者工具(如图12-3所示)。特别是Chrome插件
,我们可以用它监控应用中的state以及分发action。

图12-3 带有Redux开发工具的计数器应用
Redux DevTools最棒的一点是,它可以让我们清楚地观察到每个action如何流经本系统以及它对state的影响。
现在就去安装Redux DevTools中的Chrome插件吧!
要想使用开发者工具,我们必须先做一件事:把它添加到store中。
code/redux/angular2-redux-chat/minimal/app.ts
let devtools:StoreEnhancer<AppState> =
window['devToolsExtension'] ?
window['devToolsExtension']():f => f;
并不是每个使用我们应用的人都安装好了Redux DevTools。上述代码会检查由Redux DevTools定义的window.devToolsExtension。如果它存在,我们就使用它;否则返回一个identity function(f => f),它会直接返回传给它的一切。

middleware是一个术语,表示用来强化另一个类库功能的函数。Redux DevTools是众多Redux middleware类库中的一个。Redux支持许多有趣的middleware,如果想自己写也很容易。
你可以在http://redux.js.org/docs/advanced/Middleware.html读到关于Redux middleware的更多内容。
为了使用这个devtools,我们把它当作middleware传给Redux的store。
code/redux/angular2-redux-chat/minimal/app.ts
let store:Store<AppState> = createStore<AppState>(
counterReducer,
devtools
);
现在,无论我们分发action还是改变state,都可以在浏览器中监测到了。
现在已经设置好了Redux的内核,我们把注意力转向Angular组件。先来创建应用的顶层组件CounterApp。它将被用来引导(bootstrap)Angular。
code/redux/angular2-redux-chat/minimal/app.ts
@Component({
selector:'minimal-redux-app',
template:`
<div>
<counter-component>
</counter-component>
</div>
`
})
class CounterApp {
}
这个组件所做的一切就是创建CounterComponent的一个实例,我们马上就会定义它。在此之前,让我们先来启动应用。
我们将用CounterApp作为应用的根组件。记住,由于这是一个Redux应用,我们需要让store的实例在应用的任何地方都能被访问到。该怎么做呢?我们将使用依赖注入技术。
还记得第8章提到过的吗?当希望通过依赖注入来获取某样东西时,我们就会在NgModule中使用providers配置项将其添加到providers列表中。
如果我们要把某样东西提供给依赖注入系统,需要指出两点:
(1)用于指代这个可注入依赖的令牌;
(2)注入依赖的方式。
通常,如果我们想提供一个单例服务,可能会像这样使用useClass选项:
{ provide:SpotifyService, useClass:SpotifyService }
在这个例子中,我们使用SpotifyService类作为依赖注入系统中的令牌。useClass选项会告诉Angular创建SpotifyService的一个实例,并且无论何时要求注入SpotifyService都会复用这个实例(也就是维护一个单例)。
不过使用这种方式有一个问题:我们不想让Angular创建store,因为之前已经用createStore创建好了。我们只想使用已创建好的store。
要这么做,就要使用provide中的useValue选项。之前我们已经使用过像API_URL这样的可配置值了:
{ provide:API_URL, useValue:'http://localhost/api' }
还有一件事没有解决,那就是使用什么样的令牌来注入。store的类型是Store<AppState>。
code/redux/angular2-redux-chat/minimal/app.ts
let store:Store<AppState> = createStore<AppState>(
counterReducer,
devtools
);
Store并非一个类,而是一个接口。很不幸,我们不能使用接口作为依赖注入的键。

你也许想知道接口为什么不能作为依赖注入的键。答案就是,因为TypeScript的接口在编译完成后就会被移除,所以在运行环境中是获取不到的。
如果你想了解更多,请参见http://stackoverflow.com/questions/32254952/binding-a-class-to-an-interface、https://github.com/angular/angular/issues/135和http://victor-savkin.com/post/126514197956/dependency-injection-in-angular-1-and-angular-2。
这就表示我们需要创建自己的令牌,用来注入store。谢天谢地,Angular让这项任务变得很容易。我们在store的文件中创建这个令牌,这样就可以在应用的任何地方导入它。
code/redux/angular2-redux-chat/minimal/app-store.ts
import { OpaqueToken } from '@angular/core';
export const AppStore = new OpaqueToken('App.store');
这里创建了一个const AppStore,它使用Angular提供的OpaqueToken类。相对于直接注入字符串,OpaqueToken是一个更好的选择,因为它有助于避免命名冲突。
现在我们可以在provide中使用AppStore这个令牌了。开工!
回到app.ts文件,我们创建一个NgModule来启动应用。
code/redux/angular2-redux-chat/minimal/app.ts
@NgModule({
declarations:[
CounterApp,
CounterComponent
],
imports:[ BrowserModule ],
bootstrap:[ CounterApp ],
providers:[
{provide:AppStore, useValue:store }
]
})
class CounterAppAppModule {}
platformBrowserDynamic().bootstrapModule(CounterAppAppModule)
现在我们就可以通过注入AppStore在应用的任何地方引用Redux的store了。目前最需要它的地方就是CounterComponent。
随着设置的完成,我们可以开始创建组件了。它实际上负责向用户显示计数器并提供按钮来让用户改变state。
12.11.1 import
我们先来看看导入。
code/redux/angular2-redux-chat/minimal/CounterComponent.ts
import {
Component,
Inject
} from '@angular/core';
import { Store } from 'redux';
import { AppStore } from './app-store';
import { AppState } from './app-state';
import * as CounterActions from './counter-action-creators';
我们从Redux中导入了Store以及我们自己的注入令牌AppStore,它可以让我们引用到store的单例。我们还导入了AppState类型,这有助于我们掌握中央state的结构。
最后,我们通过* as CounterActions语法导入了所有的action creator。这个语法会让我们调用CounterActions.increment()来创建一个INCREMENT action。
12.11.2 模板
我们来看看CounterComponent的模板(如图12-4所示)。
code/redux/angular2-redux-chat/minimal/CounterComponent.ts
@Component({
selector:'counter-component',
template:`
<div class="row">
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="caption">
<h3>Counter</h3>
<p>Custom Store</p>
<p>
The counter value is:
<b>{{ counter }}</b>
</p>
<p>
<button(click)="increment()"
class="btn btn-primary">
Increment
</button>
<button(click)="decrement()"
class="btn btn-default">
Decrement
</button>
</p>
</div>
</div>
</div>
</div>
`

图12-4 计数器应用的模板
这里有三点需要注意:
(1){{ counter }}用来显示计数器的值;
(2)点击一个按钮时会调用increment();
(3)点击另一个按钮时会调用decrement()。
12.11.3 constructor
因为这个组件依赖于Store,所以我们要在构造函数中把它注入进来。这里示范的是我们如何使用自定义的AppStore令牌来注入依赖。
code/redux/angular2-redux-chat/minimal/CounterComponent.ts
export default class CounterComponent {
counter:number;
constructor(@Inject(AppStore)private store:Store<AppState>){
store.subscribe(()=> this.readState());
this.readState();
}
readState(){
let state:AppState = this.store.getState()as AppState;
this.counter = state.counter;
}
increment(){
this.store.dispatch(CounterActions.increment());
}
decrement(){
this.store.dispatch(CounterActions.decrement());
}
}
我们使用@Inject注解来注入AppStore。注意,我们把变量store的类型定义成了Store<AppState>。这里使用的注入令牌和用类作为注入令牌时(Angular能推断出要注入的是什么)略有不同。
我们把store设置为一个实例变量(使用private store)。有了store,我们就可以监听它的变化了。这里调用了store.subscribe和this.readState();下面会定义readState。
只有当一个新的action被分发时,store才会调用subscribe,因此在这里需要确保至少手动调用readState一次,以保证组件可以获取到初始数据。
readState方法从store中读取state并把this.counter更新成最新值。因为this.counter是类的一个属性并在视图中绑定,所以Angular会检测到它发生了变化并重新渲染组件。
我们定义了两个辅助方法increment和decrement,它们分别把各自的action分发到store中。
12.11.4 整合
下面是CounterComponent的完整代码清单。
code/redux/angular2-redux-chat/minimal/CounterComponent.ts
import {
Component,
Inject
} from '@angular/core';
import { Store } from 'redux';
import { AppStore } from './app-store';
import { AppState } from './app-state';
import * as CounterActions from './counter-action-creators';
@Component({
selector:'counter-component',
template:`
<div class="row">
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="caption">
<h3>Counter</h3>
<p>Custom Store</p>
<p>
The counter value is:
<b>{{ counter }}</b>
</p>
<p>
<button(click)="increment()"
class="btn btn-primary">
Increment
</button>
<button(click)="decrement()"
class="btn btn-default">
Decrement
</button>
</p>
</div>
</div>
</div>
</div>
`
})
export default class CounterComponent {
counter:number;
constructor(@Inject(AppStore)private store:Store<AppState>){
store.subscribe(()=> this.readState());
this.readState();
}
readState(){
let state:AppState = this.store.getState()as AppState;
this.counter = state.counter;
}
increment(){
this.store.dispatch(CounterActions.increment());
}
decrement(){
this.store.dispatch(CounterActions.decrement());
}
}
试一下(如图12-5所示)!
cd code/redux/angular2-redux-chat
npm install
npm run go
open http://localhost:8080/minimal.html

图12-5 工作中的计数器应用
恭喜!你已经创建了第一个Angular和Redux应用!
现在我们已经使用Redux和Angular构建了一个基本的应用,还应该尝试构建一个更复杂的应用。当试图构建更大型的应用时,我们会遭遇新的挑战。
●如何组合使用reducer?
●如何从state的不同分支中提取数据?
●如何组织Redux代码?
在下一章中,我们将构建一个聊天应用,并在其中处理所有这些问题!
如果你想学习更多关于Redux的知识,下面是一些很不错的资源。
●Redux官网:http://redux.js.org/
●Redux作者的视频教程:https://egghead.io/courses/getting-started-with-redux
●真实世界中的Redux(幻灯片展示):https://speakerdeck.com/chrisui/real-world-redux
●强大的高阶reducer:http://slides.com/omnidan/hor
要学习更多如何结合使用Redux和Angular内容,请查阅以下资源。
●angular2-redux:https://github.com/InfomediaLtd/angular2-redux
●ng2-redux:https://github.com/angular-redux/ng2-redux
●ngrx/store:https://github.com/ngrx/store
继续前进吧!