目录
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.工具 & 环境 & 学习资料
- 工具: dva-cli 脚手架
- 环境:
nodejs 6.11.1
&npm 5.3.0
- 学习教程: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 启动项目
-
npm start
启动项目 - 浏览器打开
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';