近期在学习React,练习项目上用到了dva,在这里记录一些总结内容。
dva.js简介
dva 是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
初始化
安装 dva-cli 用于初始化项目:
npm install -g dva-cli
# 或
yarn global add dva-cli
创建项目目录,并初始化
mkdir your-project
cd your-project
#初始化项目
dva init
然后运行 npm start
或 yarn start
即可运行项目。
项目目录
项目初始化以后,默认的目录结构如下:
其中:
- mock 存放用于 mock 数据的文件;
- public 一般用于存放静态文件,打包时会被直接复制到输出目录(./dist);
- src 文件夹用于存放项目源代码;
- asserts 用于存放静态资源,打包时会经过 webpack 处理;
- components 用于存放 React 组件,一般是该项目公用的无状态组件;
- models 用于存放模型文件
- routes 用于存放需要 connect model 的路由组件;
- services 用于存放服务文件,一般是网络请求等;
- utils 工具类库
- router.js 路由文件
- index.js 项目的入口文件
- index.js 项目的入口文件
- editorconfig 编辑器配置文件
- .eslintrc ESLint配置文件
- .roadhogrc.mock.js Mock配置文件
- .webpackrc 自定义的webpack配置文件,JSON格式,如果需要 JS 格式,可修改为 .webpackrc.js
Mock
如需 mock 功能,在 .roadhogrc.mock.js
中添加配置即可,如:
如上配置,当请求 /api/users
时会返回 JSON 格式的数据。
dva-loading
dva-loading 是一个用于处理 loading 状态的 dva 插件,基于 dva 的管理 effects 执行的 hook 实现,它会在 state 中添加一个 loading
字段(该字段可自定义),自动帮你处理网络请求的状态,不需要自己再去写 showLoading
和 hideLoading
方法。
下面以一个订单头行的例子来描述一下整个开发过程。
Model
Model 是 dva 最重要的部分,可以理解为 redux、react-redux、redux-saga 的封装。
代码示例:
import { isEmpty } from 'lodash';
import {
getResponse,
createPagination,
} from 'utils/utils';
import {queryOrderHeaders,
queryHeaderDetail,
queryOrderLines,
createOrder,
updataOrderLines,
updateOrderHeader,
queryStatusList,
deleteLines} from '../../services/hiam/orderHeaderService';
export default {
namespace: 'orderHeaders',
state: {
statusList: [], // 状态值集
orderList: {
dataSource: [],
pagination: {},
},
},
reducers: {
save(state, { payload}){
return {
...state,
...payload,
};
},
setCodeReducer(state, { payload }) {
return {
...state,
...payload,
};
},
updateStateReducer(state, { payload }) {
return {
...state,
...payload,
};
},
updateHeaderListReducer(state, { payload }) {
return {
...state,
orderList: {
...state.orderList,
...payload,
},
};
},
},
effects: {
// 查询订单头列表数据
*queryOrderHeaders({ params }, { call, put }) {
const res = yield call(queryOrderHeaders, params);
const response = getResponse(res);
if (response) {
const dataSource = response.content;
yield put({
type: 'updateHeaderListReducer',
payload: {
dataSource,
pagination: createPagination(response),
},
});
}
},
// 查询值集
*queryStatusList({ params }, { put, call }) {
const response = yield call(queryStatusList, params);
if (response && !response.failed) {
yield put({
type: 'setCodeReducer',
payload: {
statusList: response,
},
});
}
},
// 查询订单头明细
*queryDetailForm({ headerId }, { call }) {
const res = yield call(queryHeaderDetail, headerId);
const response = getResponse(res);
return response || {};
},
// 创建订单头
*createOrder({ param }, { call }) {
const response = yield call(createOrder, param);
return getResponse(response);
},
// 更新订单头
*updateOrderHeader({ data }, { call }) {
const response = yield call(updateOrderHeader, data);
return getResponse(response);
},
// 查询订单行
*queryOrderLine({ param }, { call }) {
const res = yield call(queryOrderLines, param);
const response = getResponse(res);
return response
? {
dataSource: (response.content || []).map(n => ({ key: n.lineNumber, ...n })),
pagination: createPagination(res),
}
: null;
},
// 删除订单行
*deleteLines({ payload }, { call }) {
const res = yield call(deleteLines, payload);
return getResponse(res);
},
// 更新订单行
*updataOrderLines({ data }, { call }) {
const response = yield call(updataOrderLines, data);
return getResponse(response);
},
},
};
namespace
:是该 model 的命名空间,同时也是全局 state
上的一个属性,只能是字符串,不支持使用 .
创建多层命名空间。
state
:是状态的初始值。
reducer
:类似于 redux 中的 reducer,它是一个纯函数,用于处理同步操作,是唯一可以修改 state
的地方,由 action
触发,它有 state
和 action
两个参数。
effects
:用于处理异步操作,不能直接修改 state
,由 action
触发,也可触发 action
。它只能是 generator
函数,并且有 action
和 effects
两个参数。第二个参数 effects
包含 put
、call
和 select
三个字段,put
用于触发 action
,call
用于调用异步处理逻辑,select
用于从 state
中获取数据。
put用于触发 action 。
例:yield put({ type: 'todos/add', payload: 'Learn Dva' });
call用于调用异步逻辑,支持 promise 。
例:const result = yield call(fetch, '/todos');
select用于从 state 里获取数据。
例:const todos = yield select(state => state.todos);
需要注意的是,在 model 中触发这个 model 中的 action
时不需要写命名空间,比如在 fetch
中触发 save
时是 { type: 'save' }
。而在组件中触发 action
时就需要带上命名空间了,比如在某个组件中触发 fetch
时,应该是 { type: 'user/fetch' }
。
service
这一层用来请求后端的接口,service只能由model来调用。
/**
* Created by younus on 2019/2/16.
* 20495的订单管理Service
*/
import request from 'utils/request';
import {HDIPPRACTICE, HZERO_PLATFORM} from 'utils/config';
/**
* 查询订单头列表
* @returns {Promise.<void>}
*/
export async function queryOrderHeaders(params={}){
return request(`${HDIPPRACTICE}/v1/om-order-headers`, {
query: params,
});
}
/**
* 查询单个订单头
* @param params
* @returns {Promise.<void>}
*/
export async function queryHeaderDetail(params, headerId) {
return request(`${HDIPPRACTICE}/v1/om-order-headers/${headerId}`, {
query: params,
});
}
/**
* 创建订单头
* @param params
* @returns {Promise.<void>}
*/
export async function createOrder(params) {
return request(`${HDIPPRACTICE}/v1/om-order-headers`, {
method: 'POST',
body: params,
});
}
/**
* 更新订单头
* @param params
* @returns {Promise.<void>}
*/
export async function updateOrderHeader(params) {
return request(`${HDIPPRACTICE}/v1/om-order-headers`, {
method: 'PUT',
body: params,
});
}
/**
* 查询订单行(传入头ID)
* @param params
* @returns {Promise.<void>}
*/
export async function queryOrderLines( params={}) {
return request(`${HDIPPRACTICE}/v1/om-order-lines`, {
query: params,
});
}
export async function deleteLines(params={}) {
return request(`${HDIPPRACTICE}/v1/om-order-lines`, {
method: 'DELETE',
body: params,
});
}
/**
* 批量更新行
* @param params
* @returns {Promise.<void>}
*/
export async function updataOrderLines(params) {
return request(`${HDIPPRACTICE}/v1/om-order-lines`, {
method: 'PUT',
body: params,
});
}
/**
* 查询值集
* @async
* @function queryCode
* @param {object} params - 查询条件
* @param {!string} param.lovCode - 查询条件
* @returns {object} fetch Promise
*/
export async function queryStatusList(params ) {
return request(`${HZERO_PLATFORM}/v1/lovs/value`, {
query: params,
});
}
实际上可以理解为service层是用来统一管理后端请求接口的。
index.js文件 //页面入口文件
/**
* OpenApp - 三方应用管理
* @date: 2018-10-8
* @author: wangjiacheng <jiacheng.wang@hand-china.com>
* @version: 0.0.1
* @copyright Copyright (c) 2018, Hand
*/
import React from 'react';
import { connect } from 'dva';
import { isEmpty } from 'lodash';
import { Button, Form, Input } from 'hzero-ui';
import { Bind } from 'lodash-decorators';
import { Header, Content } from 'components/Page';
import intl from 'utils/intl';
import prompt from 'utils/intl/prompt';
import notification from 'utils/notification';
import { enableRender } from 'utils/renderer';
import QueryFrom from './QueryFrom';
import HeaderList from './HeaderList';
import EditDrawer from './EditDrawer';
const FormItem = Form.Item;
const viewTitlePrompt = 'hiam.roleManagement.view.title';
@Form.create({ fieldNameProp: null })
/**
* 三方应用管理
* @extends {Component} - PureComponent
* @reactProps {Object} openApp - 数据源
* @reactProps {Object} loading - 数据加载状态
* @reactProps {Object} form - 表单对象
* @reactProps {Function} [dispatch=function(e) {return e;}] - redux dispatch方法
* @return React.element
*/
@prompt({ code: 'hiam.openApp' })
@connect(({ loading={}, orderHeaders }) => ({
orderHeaders,
loading: {
effects: {
queryOrderHeaders: loading.effects['orderHeaders/queryOrderHeaders'],
queryStatusList: loading.effects['orderHeaders/queryStatusList'],
queryDetailForm: loading.effects['orderHeaders/queryDetailForm'],
queryOrderLine: loading.effects['orderHeaders/queryOrderLine'],
deleteLines: loading.effects['orderHeaders/deleteLines'],
createOrder: loading.effects['orderHeaders/createOrder'],
},
},
}))
export default class OrderHeaders extends React.PureComponent {
constructor(props) {
super(props);
this.fetchList = this.fetchList.bind(this);
this.fetchOrderStatusCode= this.fetchOrderStatusCode.bind(this);
this.queryOrderLine=this.queryOrderLine.bind(this);
this.deleteOrderLine=this.deleteOrderLine.bind(this);
this.createOrder=this.createOrder.bind(this);
}
state = {
tableRows: [],
editDrawerVisible: false,
currentRowData: {},
currentRowList: [],
actionType: null,
};
/**
* componentDidMount 生命周期函数
* render后请求页面数据
*/
componentDidMount() {
this.fetchList();
this.fetchOrderStatusCode();
}
/**
* 列表查询
* @param params
*/
fetchList(params){
const { dispatch } = this.props;
dispatch({ type: 'orderHeaders/queryOrderHeaders', params }).then(() => {
const { orderHeaders } = this.props;
const { orderList } = orderHeaders;
this.setState({
tableRows: orderList.dataSource || [],
});
});
}
@Bind()
deleteOrderLine(data) {
const { dispatch } = this.props;
dispatch({
type: 'orderHeaders/deleteLines',
payload: {
data,
},
}).then((res) => {
if (res){
notification.success();
const {currentRowData}=this.state;
const {headerId}=currentRowData;
this.queryOrderLine({headerId, page: 0, size: 10});
}
});
}
@Bind()
queryOrderLine(param) {
const { dispatch } = this.props;
dispatch({
type: 'orderHeaders/queryOrderLine',
param,
}).then((res)=>{
this.setState({
currentRowList: res.dataSource,
});
});
}
/**
* 查询订单状态值集
*/
fetchOrderStatusCode(){
const { dispatch } = this.props;
dispatch({ type: 'orderHeaders/queryStatusList', params: {lovCode: 'ORDER.20495.STATUS'} });
}
/**
* @function handleSearch - 搜索表单
*/
@Bind
handleSearch() {
this.fetchOpenAppList({ page: {} });
}
/**
* @function handleResetSearch - 重置查询表单
*/
@Bind
handleResetSearch() {
this.props.form.resetFields();
}
/**
* @function 关闭Drawer按钮事件
*/
closeDetail() {
this.setState({
actionType: null,
currentRowData: {},
editDrawerVisible: false,
currentRowList: [],
});
}
/**
* 新建订单按钮事件
*/
openDetail() {
this.setState({
editDrawerVisible: true,
actionType: 'create',
});
}
/**
* handleAction - 表格按钮事件函数
* @param {!string} action - 事件类型
* @param {!object} record - 当前行数据
*/
handleAction(action, record) {
const openDetail = (actionType, options = {}) => {
if (!isEmpty(actionType)) {
this.queryOrderLine({headerId: record.headerId, page: 0, size: 10});
this.setState({
actionType,
currentRowData: actionType === 'edit' || actionType === 'view' ? record : {},
editDrawerVisible: true,
...options,
});
}
};
const defaultAction = {
edit: () => {
// this.redirectEdit(record.id);
openDetail('edit');
},
view: () => {
// this.redirectView(record.id);
openDetail('view');
},
};
if (defaultAction[action]) {
defaultAction[action]();
}
}
createOrder(param){
const { dispatch } = this.props;
dispatch({
type: 'orderHeaders/createOrder',
param,
}).then((res)=>{
if(res){
notification.success();
const {currentRowData, actionType}=this.state;
if (actionType==='view'||actionType==='edit'){
const {headerId}=currentRowData;
this.queryOrderLine({headerId, page: 0, size: 10});
this.fetchList();
}else{
this.fetchList();
this.setState({
editDrawerVisible: false,
actionType: null,
currentRowData: {},
});
}
}
});
}
render() {
const { orderHeaders = {}, loading = {}} = this.props;
const { orderList, statusList=[] } =orderHeaders;
const { effects } = loading;
const { tableRows,
editDrawerVisible,
currentRowData,
actionType,
currentRowList,
} =this.state;
const searchProps = {
ref: node => {
this.queryForm = node;
},
handleQueryList: this.fetchList,
statusList,
loading: effects.queryOrderHeaders,
};
const listProps = {
tableRows,
statusList,
dataSource: orderList.dataSource || [],
pagination: orderList.pagination || {},
loading: effects.queryOrderHeaders,
handleAction: this.handleAction.bind(this),
};
const drawerTitle = {
view: intl.get(`${viewTitlePrompt}.content.viewRole`).d(`查看订单明细`),
edit: intl.get(`${viewTitlePrompt}.content.editRole`).d(`修改订单`),
create: intl.get(`${viewTitlePrompt}.createRole`).d('创建订单'),
};
const editDrawerProps ={
editDrawerVisible,
headerId: currentRowData.headerId,
actionType,
processing: {
query: effects.queryDetailForm,
create: effects.createOrder,
delete: effects.deleteLines,
},
onCancel: this.closeDetail.bind(this),
// save: this.saveOrderDetail.bind(this),
// create: this.createOrder.bind(this),
orderStatusCode: statusList,
detailTitle: drawerTitle[actionType],
orderDetail: currentRowData,
orderList: currentRowList,
handleDelete: this.deleteOrderLine,
handleQuery: this.queryOrderLine,
handleCreate: this.createOrder,
}
return (
<React.Fragment>
<Header title={intl.get('hiam.orderHeader.model.message.title').d('销售订单管理')}>
<Button icon="plus" type="primary" onClick={this.openDetail.bind(this)}>
{intl.get('hzero.common.button.create').d('新建订单头')}
</Button>
</Header>
<Content>
<QueryFrom {...searchProps} />
<br />
<HeaderList {...listProps} />
</Content>
<EditDrawer ref={(m) => {this.editDrawer=m;}} {...editDrawerProps} />
</React.Fragment>
);
}
}
这边引入了自定义的组件QueryForm及HeaderList
QueryForm:
import React, { PureComponent } from 'react';
import { Button, Form, Input, Row, Col, Select} from 'hzero-ui';
import intl from 'utils/intl';
import Lov from 'components/Lov';
const { Option } = Select;
const formCol = { span: 7 };
const formItemLayout = {
labelCol: {
span: 10,
},
wrapperCol: {
span: 14,
},
};
@Form.create({ fieldNameProp: null })
export default class QueryForm extends PureComponent {
constructor(props) {
super(props);
this.handleFormReset=this.handleFormReset.bind(this);
this.handleSearch=this.handleSearch.bind(this);
}
handleSearch() {
const { handleQueryList = e => e, form: { getFieldsValue = e => e } } = this.props;
const data = getFieldsValue() || {};
handleQueryList({
...data,
});
}
handleFormReset() {
const { form: { resetFields = e => e } } = this.props;
resetFields();
}
render() {
const {statusList = [], form: { getFieldDecorator = e => e } } = this.props;
return (
<Form>
<Row>
<Col {...formCol}>
<Form.Item
{...formItemLayout}
label={intl.get('hiam.orderHeader.model.companyName').d('公司名称')}
>
{getFieldDecorator('companyId')(<Lov code="ORDER.20495.COMPANY" />)}
</Form.Item>
</Col>
<Col {...formCol}>
<Form.Item
{...formItemLayout}
label={intl.get('hiam.orderHeader.model.customerName').d('客户名称')}
>
{getFieldDecorator('customerId')(<Lov code="ORDER.20495.CUSTOMERS" />)}
</Form.Item>
</Col>
<Col {...formCol}>
<Form.Item
{...formItemLayout}
label={intl.get('hiam.orderHeader.model.orderCode').d('销售订单号')}
>
{getFieldDecorator('orderNumber')(<Input />)}
</Form.Item>
</Col>
<Col {...formCol}>
<Form.Item
{...formItemLayout}
label={intl.get('hiam.orderHeader.model.inventoryName').d('物料')}
>
{getFieldDecorator('inventoryItemId')(<Lov code="ORDER.20495.ITEM" />)}
</Form.Item>
</Col>
<Col {...formCol}>
<Form.Item
{...formItemLayout}
label={intl.get('hiam.orderHeader.model.inventoryName').d('订单状态')}
>
{getFieldDecorator('orderStatus')(
<Select allowClear >
{statusList.map(n => (
<Option key={n.value} value={n.value}>
{n.meaning}
</Option>
))}
</Select>
)}
</Form.Item>
</Col>
<Col span={7} offset={2} className="search-btn" >
<Form.Item>
<Button type="primary" htmlType="submit" onClick={this.handleSearch}>
{intl.get('hzero.common.button.search').d('查询')}
</Button>
<Button style={{ marginLeft: 8 }} onClick={this.handleFormReset}>
{intl.get('hzero.common.button.reset').d('重置')}
</Button>
</Form.Item>
</Col>
</Row>
</Form>
);
}
}
HeaderList:
/**
* Created by younus on 2019/2/16.
*/
/**
* Table - 角色管理 - 列表页面表格
* @date: 2018-7-4
* @author: lijun <jun.li06@hand-china.com>
* @version: 0.0.1
* @copyright Copyright (c) 2018, Hand
*/
import React, { PureComponent, Fragment } from 'react';
import pathParse from 'path-parse';
import { isEmpty, sum, isNumber } from 'lodash';
import { Table, Badge, Menu, Dropdown, Icon } from 'hzero-ui';
import { getCodeMeaning } from 'utils/utils';
import intl from 'utils/intl';
const modelPrompt = 'hiam.roleManagement.model.roleManagement';
const commonPrompt = 'hzero.common';
class HeaderList extends PureComponent {
/**
* defaultTableRowKey - 默认table rowKey
*/
defaultTableRowKey = 'headerId';
/**
* onCell - 删除角色成员钩子函数
* @param {number} maxWidth - 单元格最大宽度
*/
onCell(maxWidth) {
return {
style: {
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: maxWidth || 180,
whiteSpace: 'nowrap',
},
onClick: e => {
const { target } = e;
if (target.style.whiteSpace === 'normal') {
target.style.whiteSpace = 'nowrap';
} else {
target.style.whiteSpace = 'normal';
}
},
};
}
optionsRender(text, record) {
const { handleAction = e => e } = this.props;
const menu = (
<Menu onClick={({ key }) => handleAction(key, record)}>
{!record.disadbleView && (
<Menu.Item key="view">
<a>{intl.get(`${commonPrompt}.button.view`).d('查看')}</a>
</Menu.Item>
)}
{!record.disadbleEdit && (
<Menu.Item key="edit">
<a>{intl.get(`${commonPrompt}.button.edit`).d('编辑')}</a>
</Menu.Item>
)}
</Menu>
);
return (
<Dropdown overlay={menu} placement="bottomCenter">
<a className="ant-dropdown-link">
{intl.get(`${commonPrompt}.table.column.option`).d('操作')}
<Icon type="down" />
</a>
</Dropdown>
);
}
render() {
const {
dataSource = [],
loading,
statusList=[],
} = this.props;
const tableProps = {
rowKey: this.defaultTableRowKey,
columns: [
{
title: intl.get(`${modelPrompt}.members`).d('销售订单号'),
width: 100,
align: 'center',
onCell: this.onCell.bind(this),
dataIndex: 'orderNumber',
},
{
title: intl.get(`${modelPrompt}.members`).d('公司名称'),
dataIndex: 'companyName',
align: 'center',
width: 100,
onCell: this.onCell.bind(this),
key: 'companyName',
},
{
title: intl.get(`${modelPrompt}.members`).d('客户名称'),
align: 'center',
width: 100,
onCell: this.onCell.bind(this),
dataIndex: 'customerName',
key: 'customerName',
},
{
title: intl.get(`${modelPrompt}.parentRole`).d('订单日期'),
width: 110,
align: 'center',
onCell: this.onCell.bind(this),
dataIndex: 'orderDate',
},
{
title: intl.get(`${modelPrompt}.members`).d('订单状态'),
align: 'center',
width: 70,
onCell: this.onCell.bind(this),
dataIndex: 'orderStatus',
render: text => getCodeMeaning(text, statusList),
},
{
title: intl.get(`${modelPrompt}.members`).d('订单金额'),
width: 70,
align: 'center',
onCell: this.onCell.bind(this),
dataIndex: 'orderAmount',
},
{
title: intl.get(`${commonPrompt}.table.column.option`).d('操作'),
align: 'center',
width: 70,
render: this.optionsRender.bind(this),
},
],
dataSource,
pagination: true,
loading,
bordered: true,
};
tableProps.scroll = {
x: sum(tableProps.columns.map(n => (isNumber(Number(n.width)) ? n.width : 0))),
};
return <Table {...tableProps} />;
}
}
export default HeaderList;
路由:
// 订单管理
'/hiam/orders-20495': {
component: dynamicWrapper(app, ['hiam/orderHeaders'], () =>
import('../routes/hiam/Orders-20495')
),
},
我个人的整个开发流程是组件->页面->model->service,由于是初次使用dva,也算是第一次的探索,先在这里记录一下过程,如果有错误,欢迎指正!