重新搭建 React - Redux - Router 完整项目

重新搭建 React - Redux - Router 完整项目

说明

重新看了一遍 React 16.2.0 的文档,针对 react 的变化,以及使用习惯做了重新的总结,上一篇 重新搭建 react 开发环境 已经完成,这里继续针对 react 项目搭建进一步记录。

源码

开发环境搭建

react 升级到 16

重要文件版本

node: v8.9.0 nam: v5.6.0 yarn: v1.3.2
react: v16.2.0 redux: v3.7.2 react-router-dom: v4.2.2
react-redux: v5.0.6 react-router-redux: v5.0.0-alpha.9 react-hot-loader: v4.0.0-beta.12

目录结构 /src/

    ├── api
    │   ├── request.js // 网络请求的封装文件
    │   └── home.js // 抽取某个页面的网络请求部分
    ├── assets // 资源文件夹 图片、音频、视频等
    ├── components // 展示型组件,按照容器组件分文件夹
    │   ├── App // App 容器组件的展示型子组件目录
    │       └── index.js // 顶层zi'zu'jian
    ├── containers // 容器组件
    │   └── App.js
    ├── modules // redux 模块,包括 reducer 部分与 action 部分
    │   ├── home // 对应某个容器组件,集中了这个容器的 reducer 和 action
    │   │   ├── actions.js
    │   │   └── reducer.js
    │   ├── reducers.js // 合并后的总的 reducer
    │   └── types-constant.js // 抽取出的 type 常量
    ├── scss // 样式文件
    │   ├── _common.scss
    │   └── home.scss
    ├── store // store 配置
    │   ├── configureStore.js
    │   └── index.js
    └── utils // 公用的工具函数
    │   └── bindActions.js
    ├── index.html // 页面模板
    ├── routes.js // 路由配置
    └── index.js // react 入口

文件说明

1. 入口文件 /index.js

    import './scss/index.scss';

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { Provider } from 'react-redux';
    import { ConnectedRouter } from 'react-router-redux';
    import createHistory from 'history/createBrowserHistory';
    // 引入 store 
    import store from './store/index';
    // 引入路由配置组件
    import Root from './routes';

    const history = createHistory();
    const mountNode = document.getElementById('app');
    /*
       react-redux 提供 Provider 组件,
       被 Provider 组件包裹的整个 APP 中的每个组件,都可以通过 connect 去连接 store 
    */
    ReactDOM.render((
       <Provider store={store}>
          <ConnectedRouter history={history} basename="">
             <Root/>
          </ConnectedRouter>
       </Provider>
    ), mountNode);
    // 热模块更新 改为在 routes.js 中定义

2. 创建 store /store/index.js

3. 得到创建 store 的方法/store/configureStore.js

  • 添加中间件
  • 链接浏览器调试工具
  • 通过 compose 返回增强后的 createStore 函数
    import { compose, createStore, applyMiddleware } from 'redux';
    import { routerMiddleware } from 'react-router-redux';
    // 引入thunk 中间件,处理异步操作
    import thunk from 'redux-thunk';
    import createHistory from 'history/createBrowserHistory';

    const history = createHistory();
    // 生成 router中间件
    const routeMiddleware = routerMiddleware(history);
    const middleware = [routeMiddleware, thunk];

    /*
        辅助使用chrome浏览器进行redux调试
    */
    // 判断当前浏览器是否安装了 REDUX_DEVTOOL 插件
    const shouldCompose =
       process.env.NODE_ENV !== 'production' &&
       typeof window === 'object' &&
       window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;

    /*
       如果浏览器安装的 redux 工具,则使用 redux 工具 扩展过的 compose
       compose 是一个 createStore 增强工具,
       他是一个高阶函数,最终会返回新的增强后的 createStore
    */
    const composeEnhancers = shouldCompose
       ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
            // Specify here name, actionsBlacklist, actionsCreators and other options
         })
       : compose;

    /*
       调用 applyMiddleware ,使用 middleware 来增强 createStore
    */
    const configureStore = composeEnhancers(applyMiddleware(...middleware))(
       createStore
    );

    export default configureStore;

4. 得到合并的 reducer /modules/reducers.js

modules 目录用来存放所有容器组件对应的 reducer 和 action ,最终所有容器组件的 reducer 都会合并到 /modules/reducers.js

    import { combineReducers } from 'redux';
    import { routerReducer } from 'react-router-redux';
    import home from './home/reducer'; // 引入容器组件的局部 reducer

    // 合并到主reducer
    const reducers = {
       home,
       routing: routerReducer
    };

    export default combineReducers(reducers);

5. 路由配置组件(HMR)/routes.js

react-router-dom 版本为 4.2.2, API 与 3.x.x 相比有很大差别,更多使用方法查看这里

    import React from 'react';
    import { Route, Switch, Redirect } from 'react-router-dom';
    import { hot } from 'react-hot-loader'; // 引入 热更新的 hot 方法

    import App from './containers/App';
    import Test from './containers/Test';

    const Root = () => (
       <div>
          <Switch>
             <Route path="/" render={props => (
                   <App>
                      <Switch>
                         <Route path="/" exact component={Test} />
                         <Route path="/test" component={Test} />
                         <Route render={() => <Redirect to="/" />} />
                      </Switch>
                   </App>
                )}
             />
             <Route render={() => <Redirect to="/" />} />
          </Switch>
       </div>
    );
    // [email protected] 可以在这里设置热更新模块
    export default hot(module)(Root);

6. 封装网络请求方法 /api/request.js

    /*
       API 接口配置
       axios 参考文档:https://www.kancloud.cn/yunye/axios/234845
       注意:basicRequestLink 在 /config/index.js 中配置,由 imports-loader 注入
    */
    import axios from 'axios';

    /* eslint-disable no-undef */
    const basicHost = basicRequestLink;
    console.log(basicHost);

    // 删除底部 '/'
    function deleteSlash (host) {
       return host.replace(/\/$/, '');
    }

    // 添加头部 '/'
    function addSlash (path) {
       return /^\//.test(path) ? path : `/${path}`;
    }

    // 解析参数
    function separateParams (url) {
       const [path = '', paramsLine = ''] = url.split('?');

       let params = {};

       paramsLine.split('&').forEach((item) => {
          const [key, value] = item.split('=');

          params[key] = value;
       });

       return { path, params };
    }

    // 主要请求方法
    export default function request(config) {
       let {
          method, url, data = {}, host, headers
       } = config;

       method = method && method.toUpperCase() || 'GET';

       const { path, params } = separateParams(url);

       url = host
          ? `${deleteSlash(host)}${addSlash(path)}`
          : `${deleteSlash(basicHost)}${addSlash(path)}`;

       return axios({
          url,
          method,
          headers,
          data: method === 'GET' ? undefined : data,
          params: Object.assign(method === 'GET' ? data : {}, params)
       }).catch(err => {
          // 请求出错
          console.log('request error, HTTP CODE: ', err.response.status);

          return Promise.reject(err);
       });
    }

    // 一些常用的请求方法
    export const get = (url, data) => request({ url, data });
    export const post = (url, data) => request({ method: 'POST', url, data });
    export const put = (url, data) => request({ method: 'PUT', url, data });
    export const del = (url, data) => request({ method: 'DELETE', url, data });

创建一个页面的流程

1. /containers/Home.js 下创建 home 页容器组件

    import React, {Component} from 'react';
    import HomeCom from '../components/Home/index';

    class Home extends Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (<HomeCom/>);
        }
    }

    export default Home;

2. /components/Home/index.js 下创建 Home 容器组件要引入的展示型组件

    import React, {Component} from 'react';

    export default class HomeCom extends Component {
       constructor(props) {
          super(props);
       }
       render() {
          return (
            <div className="home-container"></div>
          );
       }
    }

3. 给 home 页添加路由 /routes.js

更改之前的 /routes.js 文件

    import React from 'react';
    import { Route, Switch, Redirect } from 'react-router-dom';
    import { hot } from 'react-hot-loader';

    import App from './containers/App';
    import Home from './containers/Home';
    import Test from './containers/Test';

    const Root = () => (
       <div>
          <Switch>
             <Route path="/" render={props => (
                   <App>
                      <Switch>
                         <Route path="/" exact component={Home} />
                         <Route path="/home" component={Home} />
                         <Route path="/test" component={Test} />
                         <Route render={() => <Redirect to="/" />} />
                      </Switch>
                   </App>
                )}
             />
             <Route render={() => <Redirect to="/" />} />
          </Switch>
       </div>
    );

    export default hot(module)(Root);

4. 创建 Home 容器对应的 reducer action

1. 创建 Home 容器的 reducer /modules/home/reducer.js

reducer 函数的作用是,根据当前的状态去返回一个新的状态,state 参数是不可变的,返回的 state 一定是一个新的状态

    // 将 action type 提取出来作为常量,防止编写错误
    import {
       CHANGE_INPUT_INFO,
       GET_MEMBER_LIST
    } from '../types-constant';

    // state 初始化数据
    const initialState = {
       memberList: [],
       inputInfo: {
          name: '',
          tel: ''
       }
    };

    /*
       对应不同 action.type 的处理函数,需要返回一个新的 state
       也可以 switch 语句 处理不同 action.type
    */
    const typesCommands = {
       [CHANGE_INPUT_INFO](state, action) {
          return Object.assign({}, state, { inputInfo: action.msg });
       },
       [GET_MEMBER_LIST](state, action) {
          return Object.assign({}, state, { memberList: action.msg });
       }
    }

    /*
       这里会输出一个 reducer 函数,
       reducer 函数的作用是,根据当前的状态去返回一个新的状态
       state 参数是不可变的,这里返回的 state 一定是一个新的状态
    */
    export default function home(state = initialState, action) {
       const actionResponse = typesCommands[action.type];

       return actionResponse ? actionResponse(state, action) : state;
    }

2. 创建 Home 容器的 createAction 函数 /modules/home/actions.js

  • action 是一个描述我们想要改变什么的对象,
    他需要有一个 属性为 type 值为 字符串 的键值对,对应着 reducer 中生成新 state 状态的规则
  • dispatch 函数仅可以通过 store.dispatch() 调用,
    如下 createAction 中的 dispatch 是传入的参数,为了配合 redux bindActionCreators() 函数使用
    // 将 action.type 抽取为常量,减少出错
    import {CHANGE_INPUT_INFO, GET_MEMBER_LIST} from '../types-constant';
    // 将网络请求抽取出来,方便接口调试,函数返回 Promise
    import {obtainMemberList, postNewMember} from '../../api/home';

    // 获取 成员信息列表, 
    export function getMemberList() {
        return dispatch => {
            obtainMemberList()
                .then(res => dispatch({type: GET_MEMBER_LIST, msg: res}))
                .catch(err => console.log('error ', err));
        }
    }

    // 改变新增的本地的成员信息
    export function changeInputInfo(newMember) {
        return {type: CHANGE_INPUT_INFO, msg: newMember};
    }

    /*
        可以使用 async await 返回处理异步,返回的是一个 promise 
        如果发生错误,可以在这里使用 try catch 捕获,或者直接交给调用 action 的部分 通过 Promise 的 catch 方法捕获
    */
    // 提交新成员信息
    export function postNewInfo(newMember) {
        console.log('newMember', newMember);
        return async dispatch => {
            await postNewMember(newMember)
            const newData = await obtainMemberList();

            dispatch({type: GET_MEMBER_LIST, msg: newData});

            return 'success';
        }
    }

3. 将提取出的网络请求代码写在 /api/home.js

    import {get, post} from './request';

    export function obtainMemberList() {
       return get('/list').then(res => res.data);
    }

    export function postNewMember(newMember) {
       if (!newMember.name) {
          return Promise.reject('name is wrong');
       }
       return post('/list', newMember).then(res => res.data);
    }

4. 将 reducer 合并到 主支上 /modules/reducers.js

    import { combineReducers } from 'redux';
    import { routerReducer } from 'react-router-redux';
    import home from './home/reducer';

    // 合并到主reducer
    const reducers = {
       home,
       routing: routerReducer
    };

    // combineReducers() 函数用于将分离的 reducer 合并为一个 reducer 
    export default combineReducers(reducers);

5. 将 Home 容器与 Redux 的 store 联通 /containers/Home.js

修改 Home 组件代码

  1. connect 是一个高阶函数,调用后会返回一个函数
  2. 再次传入一个组件作为参数调用后,会返回一个新的包装过的组件
  3. connect 的作用是链接组件与 Redux store,前提是这个组件外层有 Provider 组件包裹
  4. connect 首次调用接受两个可选参数,
    • (1). mapStateToProps 函数,接收 store 中 state 作为参数,
      返回的状态作为属性注入到组件中
    • (2). mapDispatchToProps 函数,接收 store.dispatch 作为参数,
      返回一个对应 actionCreator 的对象
  5. bindActionCreators 方法由 Redux 提供,
    用来把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们
    import React, {Component} from 'react';
    import {connect} from 'react-redux';
    import {bindActionCreators} from 'redux';

    import HomeCom from '../components/Home/index';
    import {getMemberList, changeInputInfo, postNewInfo} from '../modules/home/actions';

    class Home extends Component {
        constructor(props) {
            super(props);
        }
        render() {
            return (<HomeCom {...this.props}/>);
        }
    }

    export default connect(
        state => ({homeState: state.home}),
        dispatch => ({
            getMemberList: bindActionCreators(getMemberList, dispatch),
            changeInputInfo: bindActionCreators(changeInputInfo, dispatch),
            postNewInfo: bindActionCreators(postNewInfo, dispatch),
        })
    )(Home);

参考

猜你喜欢

转载自blog.csdn.net/mjzhang1993/article/details/79022633