在学习React中,我们必定逃脱不了Redux来解决我们遇到的数据流问题,这儿根据《深入React技术栈》写的一个实例。
代码放在我的github上
初始化 Redux 项目
建立一个文件
mkdir redux-blod && cd redux-blog
新增一个 package.json文件,安装需要的依赖
npm install --save react react-dom redux react-router react-redux react-router-redux whatwg-fetch
划分目录结构:
.
├── node_modules
└── package.json
我们把所有源文件放在 src/ 目录下,
把测试文件放在 test/ 目录下,
把最终生成的、供HTML引用的文件放在 build/ 目录下
$ mkdir src
$ mkdir test
$ mkdir build
src中目录划分即采用类型划分的特点,又添加了功能划分的特点。
基本上,我们只需要关注 views/ 和 components/ 这个两个文件夹
设计路由
src/
├── components
│ ├── Detail 文章详情页
│ └── Home 文章列表页
└── views
├── Detail.css
├── Detail.js
├── DetailRedux.js
├── Home.css
├── Home.js
└── HomeRedux.js
按照我们的目录结构,所有的路由应该放在 src/routes/ 目录下,因此在这个目录下新建 index.js 文件,用来配置整个应用的所有路由信息
src/
├── components
├── routes
│ └── index.js
└── views
在index文件中,我们引入所有需要的依赖
// routes/index.js
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
import Home from '../views/Home';
import Detail from '../views/Detail';
//接下来,使用 react-router 提供的组件来定义应用的路由:
const routes = (
);
优化构建脚本
添加 webpack-dev-server 作为项目依赖
$ npm install -D webpack-dev-server
将下面的脚本添加到 npm scripts中,我们后续用 npm run watch 命令执行
./node_modules/.bin/webpack-dev-server --hot --inline --content-base
添加布局文件
在 package.json
的 scripts 中添加一条新的记录可以解决这个问题:"watch":"./node_modules/.bin/webpack --watch"
。然后在终端中执行 npm run watch 命令。
新建src/layouts 目录,添加两个文件 --Frame.js和 Nav.js
src/
├── components
├── layouts
│ ├── Frame.js
│ └── Nav.js
├── routes
└── views
// Nav.js
import React, { Component } from 'react';
import { Link } from 'react-router';
class Nav extends Component {
render() {
return (
)
}
}
引入一个新的组件 Frame.js
import React, { Component } from 'react';
import Nav from './Nav';
class Frame extends Component {
render() {
return (
{this.props.children}
);
}
}
对index.js进行改造
import React from 'react';
import { Router, Route, IndexRoute, hashHistory } from 'react-router';
import Frame from '../layouts/Frame';
import Home from '../views/Home';
import Detail from '../views/Detail';
const routes = {
}
export default routes;
准备首页数据
在src/components/Home/ 文件夹下添加几个新文件
src/
├── components
│ ├── Detail
│ └── Home
│ ├── Preview.css
│ ├── Preview.js
│ ├── PreviewList.js
│ └── PreviewListRedux.js
├── layouts
├── routes
└── views
在Preview.js 中定义一个纯渲染、无状态的文章预览组件
import React, { Component } from 'react';
import './Preview.css';
class Preview extends Component {
static propTypes = {
title: React.PropTypes.string,
link: React.PropTypes.string,
};
render() {
return (
{this.props.title}
{this.props.date}
{this.props.description}
)
}
}
PreviewList.js的代码
import React, { Component } from 'react';
import Preview from './Preview';
class PreviewList extends Component {
static propTypes = {
articleList: React.PropTypes.arrayOf(React.PropTypes.object)
};
render() {
return this.props.articleList.map(item => (
))
}
}
在介绍 Redux 应用目录结构时,我们提到过Redux.js 里包含了.js 这个组件需要的reducer、action creator 和 constants。
const initialState = {
loading: true,
error: false,
articleList: [],
};
// 3 个常量定义和一个函数定义在逻辑上属于一个整体
const LOAD_ARTICLES = 'LOAD_ARTICLES';
const LOAD_ARTICLES_SUCCESS = 'LOAD_ARTICLES_SUCCESS';
const LOAD_ARTICLES_ERROR = 'LOAD_ARTICLES_ERROR';
// 而 loadArticles() 就是一个 action creator。因为每次调用 loadArticles() 函数时,它都会返回一个 action,所以 action creator 之名恰如其分
export function loadArticles() {
return {
types: [LOAD_ARTICLES, LOAD_ARTICLES_SUCCESS, LOAD_ARTICLES_ERROR],
url: '/api/articles.json',
};
}
function previewList(state = initialState, action) {
switch (action.type) {
case LOAD_ARTICLES: {
return {
...state,
loading: true,
error: false,
};
}
case LOAD_ARTICLES_SUCCESS: {
return {
...state,
loading: false,
error: false,
articleList: action.payload.articleList,
};
}
case LOAD_ARTICLES_ERROR: {
return {
...state,
loading: false,
error: true,
};
}
default:
return state;
}
}
export default previewList;
连接 Redux
- 让容器型组件关联数据
// views/HomeRedux.js包含了 Home 页面所有组件相关的 reducer及actionCreator
import { combineReducers } from 'redux';
// 引入 reducer 及 actionCreator
import list from '../components/Home/PreviewListRedux';
export default combineReducers({
list,
});
export * as listAction from '../components/Home/PreviewListRedux'
可以看到,views/ 目录下的 *Redux.js 文件在更大程度上只是起到一个整合分发的作用。和components/ 目录下的 *Redux.js 文件一样,它默认导出的是当前路由需要的所有 reducer 的集合。这里我们引入了 Redux 官方提供的combineReducers 方法,通过这个方法,我们可以方便地将多个 reducer 合并为一个。
此外,HomeRedux.js 还 将PreviewListRedux.js 中所有导出的对象合并后,导出一个listAction 对象。稍后,就会看到我们为什么要这么组织文件。
重新对 views/Home.js做一些修改,让它和Redux连接起来
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import PreviewList from '../components/Home/PreviewList';
import { listAction } from './HomeRedux';
class Home extends Component {
render() {
Home
}
}
export default connect(state => {
return {
list: state.home.list,
}
}, dispatch => {
return {
listAction: bindActionCreators(listActions, dispatch)
}
})(Home)
connect 最多接受 4 个参数,分别如下
[mapStateToProps(state, [ownProps]): stateProps](类型:函数):接受完整的 Redux
状态树作为参数,返回当前组件相关部分的状态树,返回对象的所有 key 都会成为组件
的 props。[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (类型:对象或函数):
接受 Redux 的 dispatch 方法作为参数,返回当前组件相关部分的 action creator,并可以
在这里将 action creator 与 dispatch 绑定,减少冗余代码。[mergeProps(stateProps, dispatchProps, ownProps): props] (类型:函数):如果指定
这个函数,你将分别获得 mapStateToProps、mapDispatchToProps 返回值以及当前组件的
props 作为参数,最终返回你期望的、完整的 props。-
[options](类型:对象):可选的额外配置项,有以下两项。
- [pure = true](类型:布尔):该值设为 true 时,将为组件添加 shouldComponentUpdate()
生命周期函数,并对 mergeProps 方法返回的 props 进行浅层对比。 - [withRef = false](类型:布尔):若设为 true,则为组件添加一个 ref 值,后续可
以使用 getWrappedInstance() 方法来获取该 ref,默认为 false。
- [pure = true](类型:布尔):该值设为 true 时,将为组件添加 shouldComponentUpdate()
- 让展示型组件使用数据
相比于容器型组件与 Redux 的复杂交互,展示型组件实现起来则简单得多,毕竟一切需要的
东西都已经通过 props 传进来了
import React, { PropTypes, Component } from 'react';
import Preview from './Preview';
class PreviewList extends Component {
static propTypes = {
loading: PropTypes.bool,
error: PropTypes.bool,
articleList: PropTypes.arrayOf(PropTypes.object),
loadArticles: PropTypes.func,
};
componentDidMount() {
this.props.loadArticles();
}
render() {
const { loading, error, articleList } = this.props;
if (error) {
return Oops, something is wrong.
;
}
if (loading) {
return Loading...
;
}
return articleList.map(item => ( ));
}
- 注入Redux
在“让容器型组件关联数据 ”一节中,我们学习了如何使用 connect 方法关联 Redux 状态
树中的部分状态。问题是,完整的 Redux 状态树是哪里来的呢?
src/
├── app.js
├── components
├── layouts
├── redux
│ ├── configureStore.js
│ └── reducers.js
├── routes
└── views
先来看看 reducers.js,这个文件里汇总了整个应用所有的 reducer,而汇总的方法则十分简单。
因为我们在 views/ 文件夹中已经对各个路由需要的 reducer 做过一次整理聚合,所以在 reducers.js
中直接引用 views/*Redux.js 中默认导出的 reducer 即可。
而 configureStore.js 则是生成 Redux store 的关键文件,其中将看到 5.1 节中提到的 Redux 的
核心 API——createStore 方法
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerReducer } from 'react-router-redux';
import ThunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';
const finalCreateStore = compose(
applyMiddleware(ThunkMiddleware)
)(createStore);
const reducer = combineReducers(Object.assign({}, rootReducer, {
routing: routerReducer,
}));
export default function configureStore(initialState) {
const store = finalCreateStore(reducer, initialState);
return store;
}
新建一个实例
// app.js
import ReactDOM from 'react-dom';
import React from 'react';
import configureStore from './redux/configureStore';
import { Provider } from 'react-redux';
import { syncHistoryWithStore } from 'react-router-redux';
import { hashHistory } from 'react-router';
import routes from './routes';
const store = configureStore();
const history = syncHistoryWithStore(hashHistory, store);
ReactDOM.render((
{routes(history)}
), document.getElementById('root'));
引入 Redux Devtools
需要单独下载这些依赖
$ npm install --save-dev redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
现在讲 DevTools 初始化的相关代码统一放在 src/redux/DevTools.js 中
import React from 'react';
import { createDevTools } from 'redux-devtools';
import LogMonitor from 'redux-devtools-log-monitor';
import DockMonitor from 'redux-devtools-dock-monitor';
const DevTools = createDevTools(
);
export default DevTools;
DockMonitor 决定了 DevTools 在屏幕上显示的位置,我们可以按 Control+Q 键切换位置,或者按 Control+H 键隐藏 DevTool。而LogMonitor 决定了 DevTools 中显示的内容默认包含了 action的类型、完整的 action 参数以及 action 处理完成后新的 state。
利用 middleware 实现Ajax请求发送
利用redux-composable-fetch 这个 middleware 实现异步请求
修改configureStore
import { createStore, combineReducers, compose, applyMiddleware } from 'redux';
import { routerReducer } from 'react-router-redux';
import ThunkMiddleware from 'redux-thunk';
// 引入请求 middleware 的工厂方法
import createFetchMiddleware from 'redux-composable-fetch';
import rootReducer from './reducers';
// 创建一个请求 middleware 的示例
const FetchMiddleware = createFetchMiddleware();
const finalCreateStore = compose(
applyMiddleware(
ThunkMiddleware,
// 将请求 middleware 注入 store 增强器中
FetchMiddleware
)
)(createStore);
const reducer = combineReducers(Object.assign({}, rootReducer, {
routing: routerReducer,
}));
export default function configureStore(initialState) {
const store = finalCreateStore(reducer, initialState);
return store;
}
利用webpack-dev-server 在本地启动一个简单的http服务器来响应页面
页面之间的跳转
在 Redux 应用中,路由状态也属于整个应用状态的一部分,所以更合理的方案应该是通过分发action来更新路由
使用 react-router-redux 中提供的 routerMiddleware
// redux/configureStore.js
import { hashHistory } from 'react-router';
import { routerMiddleware } from 'react-router-redux';
import rootReducer from './reducers';
const finalCreateStore = compose(
applyMiddleware(
// 引入其他 middleware
// ...
// 引入 react-router-redux 提供的 middleware
routerMiddleware(hashHistory)
)
)(createStore);
引入新的 middleware 之后,就可以像下面这样简单修改当前路由了:
import { push } from 'react-router-redux';
// 在任何可以拿到 store.dispatch 方法的环境中
store.dispatch(push('/'))
跳转修改
// components/Home/Preview.js
import React, { Component, PropTypes } from 'react';
class Preview extends Component {
static propTypes = {
title: PropTypes.string,
link: PropTypes.string,
push: PropTypes.func,
};
handleNavigate(id, e) {
// 阻止原生链接跳转
e.preventDefault();
// 使用 react-router-redux 提供的方法跳转,以便更新对应的 store
this.props.push(id);
}
render() {
return (
{this.props.title}
{this.props.date}
{this.props.description}
);
}
}
优化与改进
调整代码以及构建脚本,最终实现在开发环境中加载 Redux DevTools,而在生产环境中不进行任何加载
要实现这样的需求,首先添加一款 webpack 插件-- DefinePlugin,这款插件允许我们定义任意的字符串,并将所有文件中包含这些字符串的地方都替换为指定值。
我们需要了解一种常见的定义 Node.js 应用环境的方法——环境变量。一般意义上来说,我们习惯使用 process.env.NODE_ENV 这个变量的值来确定当前是在什么环境中运行应用。当读取不到该值时,默认当前是开发环境;而当process.env.NODE_ENV=production 时,我们认为当前是生产环境。
而在生产环境中,配合另一款插件UglifyJS 的无用代码移除功能,可以方便地将任何不必要的依赖统统移除。
if ( process.env.NODE_ENV === 'production' ) {
// 这里的代码只会在生产环境执行
} else {
// 这里的代码只会在开发环境执行
}