通过创建一个书籍列表app, 学习Redux的一些基础概念以及主要API的运用, 下面是该app的图示:

简单介绍

首次渲染的app页面:

book-init

左边是书籍列表, 右边首次渲染的文字为提示词(选择某本书显示该本书相关信息), 功能如提示词所示, 即点击某本书后在右侧会显示该书的详细信息, 以下是图例:

book-js

book-hp

从视图层, 数据层分析

book-data-view

从上图可以看出, 从数据方面看, 该app中包含两种类型的数据, 一是左边栏中一系列的书籍(hard data), 二是当前选中的书籍(meta level properties).

从视图方面看, 该app的视图包含3部分的内容, List View是数据列表, List Item是列表中的单本书, Detail View是当前选中书籍的详细信息.

在创建app时, 我们要将app的视图与数据分开, redux负责数据层的内容, 存储state; react负责视图层的内容, 将state转化为呈现在页面上的view.

组织结构

Redux脚手架: Redux Boilerplate

文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
book_list/
README.md
node_modules/
package.json
.gitignore
.babelrc
index.html
webpack.config.js
src/
actions/
index.js
components/
app.js
containers/
book_detail.js
book_list.js
reducers/
index.js
reducer_active_book.js
reducer_books.js
index.js
style/
style.css

从文件结构中可以看到, 应用最重要的几个部分为reducers, actions, components和container. 下面对这几个部分进行详细说明.

reducer和action

reducer

reducer是返回应用state的函数, 因为大多数redux应用包含多个state, 因此包含多个不同的reducer.

在书籍应用(book_list)中, 有两类数据, 因此应包含两个reducer, 一个负责创建全部书籍列表, 一个负责创建当前选中的书籍.

book-reducers

如上图所示, 创建BooksReducer以及ActiveBook, reducer返回state的值, state的key(即图中的books和activeBook)可以为任何值, 与reducer无关, 与reducer相关的只有state的值. BooksReducer返回所有书籍列表数据, 具体内容见下:

reducer_books.js

1
2
3
4
5
6
7
8
export default function() {
return [
{ title: 'Javascript: The Good Parts', pages: 101 },
{ title: 'Harry Potter', pages: 39 },
{ title: 'The Dark Tower', pages: 85 },
{ title: 'Eloquent Ruby', pages: 1 }
];
}

ActiveBook的内容会根据用户操作时刻变化, reducer接受redux分配的action后根据action类型返回与之相关的state.

reducer_active_book.js

1
2
3
4
5
6
7
8
export default function(state = null, action) {
switch(action.type) {
case 'BOOK_SELECTED':
return action.payload;
}

return state;
}

最后再使用redux库的combineReducers方法将所有reducer整合起来. 把reducer对象传入combineReducers后, redux根据传入的所有reducer创建整个app的状态(state).

reducers/index.js

1
2
3
4
5
6
7
8
9
10
import { combineReducers } from 'redux';
import BooksReducer from './reducer_books';
import ActiveBook from './reducer_active_book';

const rootReducer = combineReducers({
books: BooksReducer,
activeBook: ActiveBook
});

export default rootReducer;

action

上面提到, ActiveBook这个reducer返回的state值会根据action.type变化, 此app比较简单, 只需要一个action来处理用户操作.

actions/index.js

1
2
3
4
5
6
export function selectBook(book) {
return {
type: 'BOOK_SELECTED',
payload: book
};
}

selectBook()是action creator, 相关的state发生改变后会调用action creator, action creator返回action, action是一个JavaScript对象, 在这个对象中必须声明type属性, type描述用户操作的具体内容, 此时是BOOK_SELECTED, 除了type属性, 还有一个payload属性, payload属性的值为与该action相关的数据值, 因为这个action选择了某一本书, 那么所需要的相关数据就是那本书的详细信息, payload的值就是所选取的书的信息. type和payload属性中, type必须声明, 但payload并非必需.

container和component

在组织组件时, 一般将组件分为container和component, 因此组织文件结构时, 也创建两个文件夹, 分别为containers和components.

book-component

容器组件(container)

container是利用redux store中数据的组件, 也被叫做smart component. BookList和BookDetail都是根据state的变化而变化, 因此这两个组件应为container.

book-detail.js

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 React, { Component } from 'react';
import { connect } from 'react-redux';

class BookDetail extends Component {
render() {
if (!this.props.book) {
return <div>Select a book to get started.</div>;
}

return (
<div>
<h3>Details for:</h3>
<div>Title: {this.props.book.title}</div>
<div>Pages: {this.props.book.pages}</div>
</div>
);
}
}

function mapStateToProps(state) {
return {
book: state.activeBook
};
}

export default connect(mapStateToProps)(BookDetail);

从以上代码中可以看到, book-detail从react-redux库中引入了connect方法, 因为在react和redux搭配使用时, react无法直接获取redux所负责的应用state, 因此需要react-redux的帮助使两个库能更好得配合使用. mapStateToProps将来自redux的state转换为当前component的props, connect将component提升为container.

关于connect:

只要应用的state产生变化(用户事件, ajax请求…):

  1. container会立即rerender
  2. 组件的props也会立即变化

book-list.js

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
32
33
34
35
36
37
38
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { selectBook } from '../actions/index';
import { bindActionCreators } from 'redux';

class BookList extends Component {
renderList() {
return this.props.books.map((book) => {
return (
<li
key={book.title}
onClick={() => this.props.selectBook(book)}
className="list-group-item">
{book.title}
</li>
);
});
}

render() {
return (
<ul className="list-group col-sm-4">
{this.renderList()}
</ul>
)
}
}

function mapStateToProps(state) {
return {
books: state.books
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ selectBook: selectBook }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(BookList);

BookList组件中, 用户点击动作不只需要调用action creator, 还要确保action分发到所有reducer中, 使用redux库中的bindActionCreator方法和dispatch方法达到此目的.

展示组件(component)

component是没有与redux store连结的组件, 也被叫做dumb component, 只负责呈现View, 不处理数据. 此例中是只负责渲染书籍列表和选中书籍的详细信息到页面中.

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import { Component } from 'react';

import BookList from '../containers/book-list';
import BookDetail from '../containers/book-detail';

export default class App extends Component {
render() {
return (
<div>
<BookList />
<BookDetail />
</div>
);
}
}

wrapup

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import App from './components/app';
import reducers from './reducers';

ReactDOM.render(
<Provider store={createStore(reducers)}>
<App />
</Provider>
, document.querySelector('.container'));

从react-redux引入了Provider, 将store作为属性传入Provider后, Provider接受来自store的更新并传递这些更新.

下面是该应用的完整逻辑介绍:

book-review

项目完整代码

Tips

React与Redux的state

只使用react构建应用时, 也有一个state, 是component state(组件层面), 要注意与redux的application state(应用层面)进行区分, 在redux应用中, 两个类型的state都可以使用, 两者是完全分离, 互不影响的, 在app中使用redux state一般是将state转化为props再使用, 利用this.props.something, 而component state是this.state.somthing.

Redux存储数据的方法与Angular, Backbone, Flux的区别

  • Redux将所有的数据只存储在一个object中, 这个object就是store.

  • 许多其他库或框架将数据分为不同的集合存储, Backbone是存储在不同的collection中, flux则是存储在不同的store中.


参考