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

目录

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';
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,978评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,954评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,623评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,324评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,390评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,741评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,892评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,655评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,104评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,451评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,569评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,254评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,834评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,725评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,950评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,260评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,446评论 2 348

推荐阅读更多精彩内容