我的Redux生态系统笔记

概述

Redux是一款用于管理前端状态的库,但是集成一些附属库有助于大大提高Redux的开发效率。本文所有的库都是在React背景下使用;本文是开发笔记,非教程,适合有一定经验的开发者阅读。

为什么要用Redux

官方文档讲了很多,传统的前端开发有很多缺点:

  1. 随着现代前端应用变得越来越庞大,需要管理的状态越来越多;
  2. 模型更新视图,视图更新模型,前端状态失控;
  3. 需求经常变动,且难以维护变化和异步性(mutation and asynchronicity)。

一句话:使用Redux是为了更好地管理状态。

Redux三大原则

  1. 单一数据源,一个前端应用只有一颗状态树;
  2. 状态只读,状态不应该被手动改变;
  3. 状态只能通过纯函数改变,纯函数只接收两个参数,前一刻的状态,以及一个动作;只有一个功能,计算下一刻的状态(必须返回一个深复制的状态对象)。

Redux生态系统

Redux生态系统越来越繁荣,为了方便Redux的开发,有很多实用性很强的库涌现了出来。

Redux中的标准动作

Flux Standard Action

Redux的思想源于Flux,Flux对于一个标准的动作是有定义的,一个FSA(Flux Standard Action)必须符合下面的条件。

一个FSA必须:

  • 是一个JSON对象;
  • 有type属性。

一个FSA可能:

  • 有payload属性;
  • 有error属性;
  • 有meta属性。

一个FSA必须不能

  • 含有除type,payload,error,meta以外的任何属性。

注意FSA中,type用于描述类型,payload用于装载数据,error用于判别当前动作是否用于描述错误(如果是true则表示是,否则其他任何值都表示否),meta用于描述其他信息(很少用到)。

Redux-Actions

Redux-Actions就是一个用于为Redux提供FSA,并且负责提供构建处理FSA的reducer的三方库。

使用Redux-Actions创建一个动作。

1
2
3
4
5
// data action types
export const ADD_USER = 'ADD_USER';

// data actions
export const addUser = createAction(ADD_USER, user => user);

创建一个Reducer。

1
2
3
4
// user list state reducer
const list = handleActions({
[RECEIVE_USERS]: (state, action) => fromJS(action.payload),
}, List([]));

上面代码片段中,handleActions()函数接收两个参数,第一个是一个对象,键是动作,值是对应的纯函数;第二个值是初始状态。

Immutable对象

注意上面的代码中的fromJS()和List([])。这两个函数是Immutable中的API。

Immutable是Facebook推出的一款解决常量和对象深复制问题的不可变集合对象的库,它的特点是所有Immutable对象都是不可变的,对Immutable对象进行的任何操作都不会在原有对象上进行改变,而是返回一个内存全新的对象。

例如我们需要创建一个Redux的初始状态,我们可以使用Immutable的Map()来创建。

1
2
3
4
5
6
7
import { Map } from 'immutable';
//...
Map({
count: 0,
curPage: 1,
perPage: 10,
});

或者当需要将一个复杂得JSON对象转换为Immutable对象时,可以使用Immutable的fromJS()方法来创建。

1
2
3
4
5
6
7
import { fromJS } from 'immutable';
//...
fromJS({
count: 0,
curPage: 1,
perPage: 10,
});

当我们需要将Immutable对象转换为JSON对象时可以使用toJS()。

1
const obj = immutableObj.toJS();

修改对象使用set(k, v)方法,获取值使用get(k),更多API,请查阅官网

Redux-Observable

Redux-Observable负责解决Redux中的异步动作问题,该库基于RxJS。推荐一个YouTube上的视频:React + Redux + RxJS = Amazing!,主讲者就是Redux-Observable的作者。

RxJS

在使用Redux-Observable之前,需要对RxJS有一个基本的了解。RxJS是一个基于Observable的响应式编程库,用于使编写基于回掉函数的代码变得更容易。官网对于RxJS的一句话解释是:可以把RxJS当成处理事件的Lodash

来看官网的示例。

1
2
3
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));

将按钮单击事件转换为一个Observable,并注册一个响应函数。

Redux-Observable处理AJAX

对于一次异步动作的处理流程,Redux-Observable引入了一个Epic(笔者习惯译为“迁徙”,待讨论)的概念。

一次迁徙就是一个动作的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import 'rxjs/Rx';
import { Observable } from 'rxjs/Observable';
import { combineEpics } from 'redux-observable';

// ...

export const fetchingUserEpic = action$ => (
action$.ofType(APP_FETCHING_USER)
.switchMap(action =>
Observable.ajax.get('http://localhost/users')
.switchMap(payload =>
Observable.concat(
Observable.of(setCurPage(action.payload)),
Observable.of(receiveUsers(payload.response)),
Observable.of(setUsersCount(+httpHeaders(payload.xhr.getAllResponseHeaders())['x-total-count'])),
Observable.of(appFetchingUserFulfilled())
)
)
.catch(() => Observable.of(appFetchingUserFailed()))
)
);

// export user epics
export const rootUserEpic = combineEpics(
fetchingUserEpic
);

上述代码定义了一个Epic,当触发了APP_FETCHING_USER事件时,发送AJAX请求,如果响应成功,则依次执行一系列后续动作,否则执行获取用户列表失败动作。

然后需要将所有的Epic作为中间件交给Redux管理。

1
2
3
4
5
6
7
8
9
10
11
// ...

const rootEpic = combineEpics(
rootUserEpic
);

const rootEpicMiddleware = createEpicMiddleware(rootEpic);

let store = createStore(app, applyMiddleware(rootEpicMiddleware));

export default store;

Redux-Form

基本使用

在实际过程中,一个应用会有很多状态需要处理,尤其是对于表单的状态,每次维护这些状态都需要写重复的代码。Redux-Form就是配合Redux解决表单状态管理的一个库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// AddForm.jsx

// ...

let AddForm = props => {
const {
handleSubmit,
} = props;
return (
<div>
<form role="form" onSubmit={handleSubmit}>
<Field name="name" type="text" label="Name" component={RenderField} validate={[required]} />
<Field name="email" type="text" label="Email" component={RenderField} validate={[required, isEmail]} />
<Field name="age" type="text" label="Age" component={RenderField} validate={[required, isWorkingAge]} />
<button className="btn btn-primary" type="submit">Submit</button>
</form>
</div>
);
};

AddForm.propTypes = {
handleSubmit: React.PropTypes.func.isRequired,
};

// 设置form属性,即该form的名字。
// 注意当需要渲染多个同类Redux-Form组件时,需要在父组件中设置form属性。
AddForm = reduxForm({
form: 'userAddForm',
})(AddForm);

export default AddForm;

上述代码中的Field组件的component属性,实际上是我们自己写的自定义组件,该组件中有且仅有一个表单元素。

1
2
3
<!-- ... -->
<input {...input} placeholder={label} type={type} />
<!-- ... -->

而validate属性中传入的数组所包含的对象都是函数类型,这些函数都接收一个参数作为待验证的值。

Validator

Validator是一款功能齐全的用于验证的库,配合Redux-Form实现表单验证功能。

1
2
3
4
5
6
7
8
9
10
11
12
export const required = value => value ? undefined : 'Required';
export const isEmail = value => {
value += '';
return validator.isEmail(value) ? undefined : 'Invalid email';
};
export const isWorkingAge = value => {
value += '';
return validator.isInt(value, {
min: 18,
max: 60,
}) ? undefined : 'Integer needed, betwwen 18 and 60';
};

这些就是AddForm.jsx中validate被赋值的数组中的元素。

React-Router 和 React-Router-Redux

使用React框架的单页应用很多都在使用React-Router做前端路由,而配合React-Router-Redux可以实现对浏览器历史路径的状态管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
import { syncHistoryWithStore } from 'react-router-redux';

const Root = ({store}) => (
<Provider store={store}>
{ /* 告诉Router使用我们修改过得history */ }
<Router history={syncHistoryWithStore(browserHistory, store)}>
<Redirect from="/" to="/home" />
<Route path="/" component={App}>
<Route path="/home" component={Dashboard}>
</Route>
<Route path="/users" component={UserModuleContainer}>
</Route>
</Route>
</Router>
</Provider>
);

仅上面这样处理一下,单页应用的history状态也会被Redux管理起来。

参考

  1. Motivation - Redux
  2. Three Principles - Redux
  3. Ecosystem - Redux
  4. Flux
  5. Flux Standard Action - GitHub
  6. Immutable - Facebook
  7. React + Redux + RxJS = Amazing! - Netflix
  8. Introduction - RxJS
  9. Redux-Observable
  10. Getting Started with redux-form - Redux-Form
  11. Validator - NPM
  12. React-Router
  13. React-Router-Redux - GitHub