美文网首页
Redux 应用实例

Redux 应用实例

作者: 不安分的三好份子 | 来源:发表于2017-10-13 17:06 被阅读0次

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

    目录划分.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 = (
     <Router history={hashHistory}>
       <Route path="/" component={Home} />
       <Route path="/detail/:id" component={Detail} />
     </Router>
    ); 
    

    优化构建脚本

    添加 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 (
          <nav>
            <Link to='/'>Home</Link>
          </nav>
        )
      }
    }
    

    引入一个新的组件 Frame.js

    import React, { Component } from 'react';
    import Nav from './Nav';
    
    class Frame extends Component {
      render() {
        return (
           <div className="frame">
             <section className="header">
               <Nav />
             </section>
             <section className="container">
               {this.props.children}
             </section>
           </div>
        );
      }
    }
    

    对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 = {
      <Router history={hashHistory}>
         <Route path='/' component={Frame}>
            <IndexRoute component={Home}>
            <Route path='/detail/:id' component={Detail} />
         </Route>
      </Router>
    }
    
    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 (
                <article className="article-preview-item">
                    <h1 className="title">{this.props.title}</h1>
                    <span className="date">{this.props.date}</span>
                    <p className="desc">{this.props.description}</p>
                </article>
            )
        }
    }
    

    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 => (
                <Preview {...item} key={item.id} />
            ))
        }
    }
    

    在介绍 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() {
            <div>
                <h1>Home</h1>
                <PreviewList
                    {...this.props.list}
                    {...this.props.listAction}
                />
            </div>
        }
    }
    
    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 <p className="message">Oops, something is wrong.</p>;
            }
            if (loading) {
                return <p className="message">Loading...</p>;
            }
            return articleList.map(item => (<Preview {...item} key={item.id} />));
        }
    
    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((
        <Provider store={store}>
            {routes(history)}
        </Provider>
    ), 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(
     <DockMonitor toggleVisibilityKey='ctrl-h'
     changePositionKey='ctrl-q'>
     <LogMonitor theme='tomorrow' />
     </DockMonitor>
    );
    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 (
                <article className="article-preview-item">
                    <h1 className="title">
                        <a href={`/detail/${this.props.id}`} onClick={this.handleNavigate.bind(this,
                            this.props.id)}>
                            {this.props.title}
                        </a>
                    </h1>
                    <span className="date">{this.props.date}</span>
                    <p className="desc">{this.props.description}</p>
                </article>
            );
        }
    } 
    

    优化与改进

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

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

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

    添加单元测试

    相关文章

      网友评论

          本文标题:Redux 应用实例

          本文链接:https://www.haomeiwen.com/subject/sxvuyxtx.html