Redux 应用实例

在学习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中目录划分即采用类型划分的特点,又添加了功能划分的特点。

Redux 应用实例_第1张图片
目录划分.png

基本上,我们只需要关注 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

  1. 让容器型组件关联数据
// 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。
  1. 让展示型组件使用数据
    相比于容器型组件与 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 => ()); }
  1. 注入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 (
            
        );
    }
} 

优化与改进

调整代码以及构建脚本,最终实现在开发环境中加载 Redux DevTools,而在生产环境中不进行任何加载
要实现这样的需求,首先添加一款 webpack 插件-- DefinePlugin,这款插件允许我们定义任意的字符串,并将所有文件中包含这些字符串的地方都替换为指定值。
我们需要了解一种常见的定义 Node.js 应用环境的方法——环境变量。一般意义上来说,我们习惯使用 process.env.NODE_ENV 这个变量的值来确定当前是在什么环境中运行应用。当读取不到该值时,默认当前是开发环境;而当process.env.NODE_ENV=production 时,我们认为当前是生产环境。

而在生产环境中,配合另一款插件UglifyJS 的无用代码移除功能,可以方便地将任何不必要的依赖统统移除。

if ( process.env.NODE_ENV === 'production' ) {
    // 这里的代码只会在生产环境执行
} else {
    // 这里的代码只会在开发环境执行
}

添加单元测试

你可能感兴趣的:(Redux 应用实例)