近期在學習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 <[email protected]>
* @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 <[email protected]>
* @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,也算是第一次的探索,先在這裏記錄一下過程,如果有錯誤,歡迎指正!