美文网首页前端社团react + dva + antd让前端飞
dva搭建简易react项目实践总结

dva搭建简易react项目实践总结

作者: ccminn | 来源:发表于2017-08-08 15:20 被阅读1260次

    目录

    1. 前言

    2. 工具 & 环境 & 学习资料

    3. 安装脚手架 & 创建react项目

    4. 设计

    4.1 抽离model
    4.2 设计组件与路由
    4.3 添加Reducers
    4.4 添加Effects
    4.5 分离service服务

    1.前言

    根据师父给的方向,因此学习了dva-cli这一个脚手架工具,进行开发React项目。
    在网上搜索学习教程的过程中,毫无疑问的浏览过一篇《12 步 30 分钟,完成用户管理的 CURD 应用 (react+dva+antd)》这一教程文章。但是在我实际的学习中,也许是由于个人软件环境(一个说法是node版本)的问题,在使用以下dva命令行时频频报错,无法跟着教程逐步搭建项目。

    // 生成名为users的路由组件
    dva g route users
    // 生成名为users的model数据模型
    dva g model users
    

    因此找到了同个作者的相对较老版本的教程,感觉在dva的介绍上更加清晰详实,最新版实在有点过于急于求成了。
    亲手跟着这篇教程完成一次实践后,基本可以了解整个react项目的数据流通过程。

    2.工具 & 环境 & 学习资料

    1. 工具: dva-cli 脚手架
    2. 环境:nodejs 6.11.1 & npm 5.3.0
    3. 学习教程:dva-cli搭建react项目user-dashboard实践教程

    3.安装脚手架 & 创建react项目

    dva结构介绍
    dva 官方中文文档
    使用 dva 所需的所有知识点

    // 安装dva-cli脚手架
    npm install -g dva-cli
    
    // 使用dva创建react项目框架
    dva new [newProjectName] 
    

    react项目的推荐目录结构(如果使用dva脚手架创建,则自动生成如下)

    |── /mock/             # 数据mock的接口文件  
    |── /src/              # 项目源码目录(我们开发的主要工作区域)   
    |   |── /components/   # 项目组件(用于路由组件内引用的可复用组件)   
    |   |── /routes/       # 路由组件(页面维度) 
    |   |  |── route1.js  
    |   |  |── route2.js   # 根据router.js中的映射,在不同的url下,挂载不同的路由组件
    |   |  └── route3.js    
    |   |── /models/       # 数据模型(可以理解为store,用于存储数据与方法)  
    |   |  |── model1.js  
    |   |  |── model2.js   # 选择分离为多个model模型,是根据业务实体进行划分
    |   |  └── model3.js  
    |   |── /services/     # 数据接口(处理前台页面的ajax请求,转发到后台)   
    |   |── /utils/        # 工具函数(工具库,存储通用函数与配置参数)     
    |   |── router.js       # 路由配置(定义路由与对应的路由组件)  
    |   |── index.js       # 入口文件  
    |   |── index.less      
    |   └── index.html     
    |── package.json       # 项目信息  
    └── proxy.config.js    # 数据mock配置  
    

    4.设计

    4.1、抽离Model

    个人理解: 此处的Model包含了一个业务实体的状态,以及方法。model与java的class其实很像,包含了自有变量(state),以及自有方法(effects),不容许外界改变自己的私有变量,但可以在其他地方通过调用Model内部的方法(effects),来修改model的变量值(在effect中调用reducer)

    抽离Model,根据设计页面需求,设计相应的Model
    教程中的需求是一个用户数据的表单展示,包含了增删改查等功能
    提出users模型

    // models/users.js
    // version1: 从数据维度抽取,更适用于无状态的数据
    // version2: 从业务状态抽取,将数据与组件的业务状态统一抽离成一个model
    // 新增部分为在数据维度基础上,改为从业务状态抽取而添加的代码
    export default {
      namespace: 'users',
      state: {
        list: [],
        total: null,
    +   loading: false, // 控制加载状态
    +   current: null, // 当前分页信息
    +   currentItem: {}, // 当前操作的用户对象
    +   modalVisible: false, // 弹出窗的显示状态
    +   modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
      },
    
        // 异步操作
        effects: {
            *query(){},
            *create(){},
            *'delete'(){},   // 因为delete是关键字,特殊处理
            *update(){},
        },
    
        // 替换状态树
        reducers: {
    +       showLoading(){}, // 控制加载状态的 reducer
    +       showModel(){}, // 控制 Model 显示状态的 reducer
    +       hideModel(){},
            querySuccess(){},
            createSuccess(){},
            deleteSuccess(){},
            updateSuccess(){},
        }
    }
    
    

    4.2、设计组件

    先设置容器组件的访问路径,再创建组件文件。

    4.2.1 两种组件概念:容器组件与展示组件
    • 容器组件:具有监听数据行为的组件,职责是绑定相关联的 model 数据,包含子组件;传入的数据来源于model
    import React, { Component, PropTypes } from 'react';
    
    // dva 的 connect 方法可以将组件和数据关联在一起
    import { connect } from 'dva';
    
    // 组件本身
    const MyComponent = (props)=>{};
    
    // propTypes属性,用于限制props的传入数据类型
    MyComponent.propTypes = {};
    
    // 声明模型传递函数,用于建立组件和数据的映射关系
    // 实际表示 将ModelA这一个数据模型,绑定到当前的组件中,则在当前组件中,随时可以取到ModelA的最新值
    // 可以绑定多个Model
    function mapStateToProps({ModelA}) {
      return {ModelA};
    }
    
    // 关联 model
    // 正式调用模型传递函数,完成模型绑定
    export default connect(mapStateToProps)(MyComponent);
    
    
    • 展示组件:展示通过 props 传递到组件内部数据;传入的数据来源于容器组件向展示组件的props
    import React, { Component, PropTypes } from 'react';
    
    // 组件本身
    // 所需要的数据通过 Container Component 通过 props 传递下来
    const MyComponent = (props)=>{}
    MyComponent.propTypes = {};
    
    // 并不会监听数据
    export default MyComponent;
    
    4.2.2 设置路由
    // .src/router.js
    import React, { PropTypes } from 'react';
    import { Router, Route } from 'dva/router';
    import Users from './routes/Users';
    
    export default function({ history }) {
      return (
        <Router history={history}>
          <Route path="/users" component={Users} />
        </Router>
      );
    };
    
    

    容器组件雏形

    // .src/routes/Users.jsx
    import React, { PropTypes } from 'react';
    
    function Users() {
      return (
        <div>User Router Component</div>
      );
    }
    
    export default Users;
    
    4.2.3 启动项目
    1. npm start启动项目
    2. 浏览器打开localhost:8000/#/users 查看新增路由与路由中的组件
    4.2.4 设计容器组件

    自顶向下的设计方法:先设计容器组件,再逐步细化内部的展示容器

    组件的定义方式:

    // 方法一: es6 的写法,当组件设计react生命周期时,可采用这种写法
    // 具有生命周期的组件,可以在接收到传入数据变化时,自定义执行方法,有自己的行为模式
    // 比如在组件生成后调用xx请求(componentDidMount)、可以自己决定要不要更新渲染(shouldComponentUpdate)等
    class App extends React.Component({});
    
    // 方法二: stateless 的写法,定义无状态组件
    // 无状态组件,仅仅根据传入的数据更新,修改自己的渲染内容
    const App = (props) => ({});
    

    容器组件:

    // ./src/routes/Users.jsx
    import React, { Component, PropTypes } from 'react';
    
    // 引入展示组件 (暂时都没实现)
    import UserList from '../components/Users/UserList';
    import UserSearch from '../components/Users/UserSearch';
    import UserModal from '../components/Users/UserModal';
    
    // 引入css样式表
    import styles from './style.less'
    
    function Users() {
    
      // 向userListProps中传入静态数据
      const userSearchProps = {};
      const userListProps = {
        total: 3,
        current: 1,
        loading: false,
        dataSource: [
          {
            name: '张三',
            age: 23,
            address: '成都',
          },
          {
            name: '李四',
            age: 24,
            address: '杭州',
          },
          {
            name: '王五',
            age: 25,
            address: '上海',
          },
        ],
      };
      const userModalProps = {};
    
      return (
        <div className={styles.normal}>
          {/* 用户筛选搜索框 */}
          <UserSearch {...userSearchProps} />
          {/* 用户信息展示列表 */}
          <UserList {...userListProps} />
          {/* 添加用户 & 修改用户弹出的浮层 */}
          <UserModal {...userModalProps} />
        </div>
      );
    }
    
    // 很关键的对外输出export;使外部可通过import引用使用此组件
    export default Users;
    

    展示组件UserList

    // ./src/components/Users/UserList.jsx
    import React, { Component, PropTypes } from 'react';
    
    // 采用antd的UI组件
    import { Table, message, Popconfirm } from 'antd';
    
    // 采用 stateless 的写法
    const UserList = ({
        total,
        current,
        loading,
        dataSource,
    }) => {
      const columns = [{
        title: '姓名',
        dataIndex: 'name',
        key: 'name',
        render: (text) => <a href="#">{text}</a>,
      }, {
        title: '年龄',
        dataIndex: 'age',
        key: 'age',
      }, {
        title: '住址',
        dataIndex: 'address',
        key: 'address',
      }, {
        title: '操作',
        key: 'operation',
        render: (text, record) => (
          <p>
            <a onClick={()=>{}}>编辑</a>
             
            <Popconfirm title="确定要删除吗?" onConfirm={()=>{}}>
              <a>删除</a>
            </Popconfirm>
          </p>
        ),
      }];
    
      // 定义分页对象
      const pagination = {
        total,
        current,
        pageSize: 10,
        onChange: ()=>{},
      };
    
    
      // 此处的Table标签使用了antd组件,传入的参数格式是由antd组件库本身决定的
      // 此外还需要在index.js中引入antd  import 'antd/dist/antd.css'
      return (
        <div>
          <Table
            columns={columns}
            dataSource={dataSource}
            loading={loading}
            rowKey={record => record.id}
            pagination={pagination}
          />
        </div>
      );
    }
    
    export default UserList;
    

    4.3 添加Reducer

    在整个应用中,只有model中的reducer函数可以直接修改自己所在model的state参数,其余都是非法操作;
    并且必须使用return {...state}的形式进行修改

    4.3.1 第一步:实现reducer函数
    // models/users.js
    // 使用静态数据返回,把userList中的静态数据移到此处
    // querySuccess这个action的作用在于,修改了model的数据
    export default {
      namespace: 'users',
      state: {},
      subscriptions: {},
      effects: {},
      reducers: {
        querySuccess(state){
            const mock = {
              total: 3,
              current: 1,
              loading: false,
              list: [
                {
                  id: 1,
                  name: '张三',
                  age: 23,
                  address: '成都',
                },
                {
                  id: 2,
                  name: '李四',
                  age: 24,
                  address: '杭州',
                },
                {
                  id: 3,
                  name: '王五',
                  age: 25,
                  address: '上海',
                },
              ]
            };
            // return 的内容是一个对象,涵盖原state中的所有属性,以实现“更新替换”的效果
            return {...state, ...mock, loading: false};
          }
      }
    }
    
    
    4.3.2 第二步:关联Model中的数据源
    // routes/Users.jsx
    
    import React, { PropTypes } from 'react';
    
    // 最后用到了connect函数,需要在头部预先引入connect
    import { connect } from 'dva';
    
    function Users({ location, dispatch, users }) {
    
      const {
        loading, list, total, current,
        currentItem, modalVisible, modalType
        } = users;
    
      const userSearchProps={};
    
      // 使用传入的数据源,进行数据渲染
      const userListProps={
        dataSource: list,
        total,
        loading,
        current,
      };
      const userModalProps={};
    
      return (
        <div className={styles.normal}>
          {/* 用户筛选搜索框 */}
          <UserSearch {...userSearchProps} />
          {/* 用户信息展示列表 */}
          <UserList {...userListProps} />
          {/* 添加用户 & 修改用户弹出的浮层 */}
          <UserModal {...userModalProps} />
        </div>
      );
    }
    
    // 声明组件的props类型
    Users.propTypes = {
      users: PropTypes.object,
    };
    
    // 指定订阅数据,并且关联到users中
    function mapStateToProps({ users }) {
      return {users};
    }
    
    // 建立数据关联关系
    export default connect(mapStateToProps)(Users);
    
    4.3.3 第三步:通过发起Action,在组件中获取Model中的数据
    // models/users.js
    // 在组件生成后发出action,示例:
    componentDidMount() {
      this.props.dispatch({
        type: 'model/action',     // type对应action的名字
      });
    }
    
    // 在本次实践中,在访问/users/路由时,就是我们获取用户数据的时机
    // 因此把dispatch移至subscription中
    // subcription,订阅(或是监听)一个数据源,然后根据条件dispatch对应的action
    // 数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等  
    // 此处订阅的数据源就是路由信息,当路由为/users,则派发'querySuccess'这个effects方法
    subscriptions: {
        setup({ dispatch, history }) {
          history.listen(location => {
            if (location.pathname === '/users') {
              dispatch({
                type: 'querySuccess',
                payload: {}
              });
            }
          });
        },
      },
    
    
    ###### 4.3.4 第四步: 在index.js中添加models
    // model必须在此完成注册,才能全局有效
    // index.js
    app.model(require('./models/users.js'));
    

    4.4 添加Effects

    Effects的作用在于处理异步函数,控制数据流程。
    因为在真实场景中,数据都来自服务器,需要在发起异步请求获得返回值后再设置数据,更新state。
    因此我们往往在Effects中调用reducer
    个人理解: 以java类做类比,effects相当于public函数,可以被外部调用,而reducers相当于private函数;当effects被调用时,间接调用到了reducer函数,修改model中的state。当然effects的核心在于异步调用,处理异步请求(如ajax请求)。

    export default {
      namespace: 'users',
      state: {},
      subscriptions: {},
      effects: {
        // 添加effects函数
        // call与put是dva的函数
        // call调用执行一个函数
        // put则是dispatch执行一个action
        // select用于访问其他model
        *query({ payload }, { select, call, put }) {
            yield put({ type: 'showLoading' });
            const { data } = yield call(query);
            if (data) {
              yield put({
                type: 'querySuccess',
                payload: {
                  list: data.data,
                  total: data.page.total,
                  current: data.page.current
                }
              });
            }
          },
        },
      reducers: {}
    }
    
    
    
    // 添加请求处理   包含了一个ajax请求
    // models/users.js
    import request from '../utils/request';
    import qs from 'qs';
    async function query(params) {
      return request(`/api/users?${qs.stringify(params)}`);
    }
    
    

    4.5 把请求处理分离到service中

    用意在于分离(可复用的)ajax请求

    // services/users.js
    import request from '../utils/request';
    import qs from 'qs';
    export async function query(params) {
      return request(`/api/users?${qs.stringify(params)}`);
    }
    
    // 在models中引用
    // models/users.js
    import {query} from '../services/users';
    

    相关文章

      网友评论

      • 2231fdc23a8f:有简单的例子吗,或者入门的视频,新手小白最近在看
      • 李逍遥JK:写的很好,赞

      本文标题:dva搭建简易react项目实践总结

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