dva介绍
根据dva官网的说法: dva 首先是一个基于
redux
和redux-saga
的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。 特性
- 易学易用,仅有 6 个 api,对 redux 用户尤其友好,
配合 umi 使用后更是降低为 0 API
- elm 概念,
通过 reducers, effects 和 subscriptions 组织 model
- 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
- 支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR
其中,
model
就是把所有跟 redux 相关的 reducer 整合到一个model文件中,通过 namespace 区分 model ,通过 state 存储数据,通过 subscriptions 实现 history 的监听,通过 effect 发起异步操作,通过 reducer 执行同步操作。
umi介绍
Umi,中文可发音为乌米,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。 umi 就是一个支持约定式路由和配置路由的大杂烩,它整合了所有的React生态的东西,antd、Dva等,把它们都当成了Umi的插件在我们需要使用的时候,直接通过配置就能使用。
案例
通过一个案例使用一下 umi 和 dva。
目录结构
结果展示
案例内容
- src/pages/users/index.tsx 主页面,用于展示主要内容
import React, {useState, useRef, useEffect, FC} from 'react'
import { Table, Button, Pagination, message } from 'antd';
import ProTable, { ProColumns } from '@ant-design/pro-table';
import { connect, Loading, Dispatch, IUserState } from 'umi'
import UserModal from './components/UserModal';
import { editRecord, addRecord } from '../../services/users';
import {ISingleUser, IFormProps} from '../../models/data'
interface IUserPageProps {
users: IUserState
dispatch: Dispatch
userListLoading: boolean
}
const Index: FC<IUserPageProps> = ({users, dispatch, userListLoading}) => {
const columns:ProColumns<ISingleUser>[] = [
{
title: 'Id',
dataIndex: 'id',
key: 'id',
render: (text: any) => <a>{text}</a>,
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'create_time',
dataIndex: 'create_time',
valueType: 'dateTime',
key: 'create_time',
},
{
title: 'Action',
key: 'action',
valueType: 'option',
render: (text: any, record: ISingleUser) => [
<Button type="primary" size="small" onClick={() => editModal(record)}>edit</Button>,
<Button danger size="small">delete</Button>
],
},
];
// 控制弹出框的显示与隐藏
const [isModalVisible, setIsModalVisible] = useState(false);
// table 中每一行的记录
const [record, setRecord] = useState<ISingleUser | undefined>(undefined);
// 修改信息
const editModal = (record: ISingleUser) => {
setIsModalVisible(true)
setRecord(record)
}
// 添加信息
const handleAdd = () => {
setRecord(undefined)
setIsModalVisible(true)
}
// 弹出框中的取消按钮
const handleClose = () => {
setIsModalVisible(false)
}
// 修改 或 新增 表单验证通过提交数据
const onFinish = async (values: IFormProps) => {
let id = 0;
if (record) {
id = record.id ? record.id : 0
}
let serverFun;
if (id) {
serverFun = editRecord
} else {
serverFun = addRecord
}
const result = await serverFun(values, id)
if (result) {
// 关闭修改框
setIsModalVisible(false)
message.success(`${id === 0 ? '添加' : '修改'}成功`)
dispatch({
type: 'users/getRemove',
payload: {
page: users.meta.page,
per_page: users.meta.per_page
}
})
}else message.error(`${id === 0 ? '添加' : '修改'}失败`)
};
// 当页码发生改变时重新获取数据
const handlePageNum = (page: number, pageSize?: number) => {
console.log(page, pageSize);
dispatch({
type: 'users/getRemove',
payload: {
page,
per_page: pageSize ? pageSize : users.meta.per_page
}
})
}
// const handlePageSize = (current: number, size: number) => {
// console.log(current, size)
// dispatch({
// type: 'users/getRemove',
// payload: {
// page: current,
// per_page: size
// }
// })
// }
// 刷新操作
const reloadHandle = () => {
dispatch({
type: 'users/getRemove',
payload: {
page: users.meta.page,
per_page: users.meta.per_page
}
})
}
return (
<div className="list-table">
{/* 表格组件 */}
<ProTable
columns={columns}
dataSource={users.data}
rowKey='id'
loading={userListLoading}
search={false}
pagination={false}
headerTitle="user list"
toolBarRender={() => [
<Button type="primary" onClick={handleAdd}>添加</Button>
]}
options={{
density: true,
fullScreen: true,
reload: () => {
reloadHandle()
},
setting: true
}}
/>
{/* 分页组件 */}
<Pagination
total={users.meta.total}
pageSize={users.meta.per_page}
showSizeChanger
showQuickJumper
showTotal={total => `共 ${total} 条数据`}
onChange={handlePageNum}
// onShowSizeChange={handlePageSize}
current={users.meta.page}
/>
<UserModal isModalVisible={isModalVisible} handleClose={handleClose} record={record} onFinish={onFinish} />
</div>
)
}
const mapStateToProps = ({users, loading}: {users: IUserState, loading: Loading}) => {
// users 为 namespace 为 users 的model,user: {}
// 所以从 model 下 返回数据时,最好是一个对象类型的数据,才不会报错 user: { data: [] }
console.log('users', users, loading);
return {
users,
userListLoading: loading.models.users
}
}
const mapDispatchToProps = (dispatch:Dispatch) => {
return {
dispatch
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Index)
- src/pages/users/components/UserModal.tsx 子组件:用于主页面使用的组件,用于添加、修改用户信息的弹出框
import React, {useState, useEffect, FC} from 'react'
import { Modal, Form, Input, DatePicker, Switch } from 'antd';
import {ISingleUser, IFormProps} from '../../../models/data'
import moment from 'moment'
interface IUserModalProps {
isModalVisible: boolean
record: ISingleUser | undefined
handleClose: () => void
onFinish: (values: IFormProps) => void
}
const UserModal: FC<IUserModalProps>= (props) => {
console.log('props', props);
const [form] = Form.useForm();
const onFinishFailed = (errorInfo: any) => {
console.log('Failed:', errorInfo);
};
useEffect(() => {
if (props.record) {
form.setFieldsValue({
...props.record,
create_time: moment(props.record.create_time),
status: props.record.status === 1 ? true : false
});
}else {
form.resetFields()
}
return () => {
};
}, [props.isModalVisible]);
const handleOk = () => {
form.submit()
}
const layout = {
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};
return (
<div>
<Modal
title={props.record ? '修改' + props.record?.id : '添加'}
visible={props.isModalVisible}
onOk={handleOk}
onCancel={props.handleClose}
forceRender
>
<Form
{...layout}
name="basic"
onFinish={props.onFinish}
onFinishFailed={onFinishFailed}
form={form}
initialValues={{
status: true
}}
>
<Form.Item
label="Name"
name="name"
rules={[{ required: true, message: 'Please input your name!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Email"
name="email"
rules={[{ required: true, message: 'Please input your email!' }]}
>
<Input />
</Form.Item>
<Form.Item
label="Create Time"
name="create_time"
>
<DatePicker showTime />
</Form.Item>
<Form.Item
label="Status"
name="status"
valuePropName="checked"
>
<Switch />
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default UserModal
- src/models/UsersModel.ts dva 中通过 state, reducers, effects 和 subscriptions 组织的 model 文件。用于数据共享,异步请求。
import { Effect, ImmerReducer, Reducer, Subscription } from 'umi';
import { getRemoveList, editRecord, addRecord } from '../services/users';
import { message } from 'antd'
import {ISingleUser} from './data'
export interface IUserState {
data: ISingleUser[],
meta: {
total: number
per_page: number
page: number
}
}
interface IUserModel {
namespace: 'users'
state: IUserState
reducers: {
getList: Reducer<IUserState>
}
effects: {
getRemove: Effect
edit: Effect
add: Effect
}
subscriptions: {
setup: Subscription
}
}
const UserModel: IUserModel = {
namespace: 'users',
state: {
data: [],
meta: {
total: 0,
per_page: 10,
page: 1
}
},
reducers: {
getList(state, action) {
return action.payload
}
},
effects: {
*getRemove({payload: {page, per_page}}, effects) {
const data = yield effects.call(getRemoveList, { page, per_page})
if (data) {
yield effects.put({
type: 'getList',
payload: data
})
}
},
*edit({payload: {id, values}}, effects) {
const data = yield effects.call(editRecord, id, values)
console.log('编辑后的结果', data)
// 调用 getRemove 方法重新获取数据
if (data) {
message.success('编辑成功!')
const { page, per_page } = yield effects.select((state: any) => state.users.meta )
yield effects.put({
type: 'getRemove',
payload: {
page,
per_page
}
})
}else {
message.error('编辑失败!')
}
},
*add({payload: {values}}, effects) {
console.log('payload', values)
const data = yield effects.call(addRecord, values)
// 调用 getRemove 方法重新获取数据
if (data) {
message.success('添加成功!')
const { page, per_page } = yield effects.select((state: any) => state.users.meta )
yield effects.put({
type: 'getRemove',
payload: {
page,
per_page
}
})
}else {
message.error('添加失败!')
}
}
},
subscriptions: {
setup({ dispatch, history }, done) {
return history.listen((location, action) => {
if(location.pathname === '/users' || location.pathname === '/my') {
// 监听路由的改变,当路由为 '/users' 时,发送 action 获取数据,返回到页面。
dispatch({
type: 'getRemove',
payload: {
page: 1,
per_page: 5
}
})
}
})
}
}
};
export default UserModel;
- src/services/users.ts 定义的request请求函数,UsersModel.ts 中调用users.ts中的请求函数,并接收返回结果。
import { message } from 'antd'
import request, { extend } from 'umi-request';
import {IFormProps} from '../models/data'
const errorHandler = function(error: any) {
if (error.response) {
// console.log(error.response.status);
// console.log(error.response.headers);
// console.log(error.data);
// console.log(error.request);
if (error.response.status > 400) {
message.error(error.data.message ? error.data.message : error.data)
}
} else {
// The request was made but no response was received or error occurs when setting up the request.
// console.log(error.message);
message.error('network error')
}
throw error;
};
// 1. Unified processing
const extendRequest = extend({ errorHandler });
// 获取数据的请求函数
export const getRemoveList = async({page, per_page}:{page: number, per_page: number}) => {
return extendRequest(`/api/users?page=${page}&per_page=${per_page}`, {
method: 'get',
})
.then((response) => {
return response
})
.catch((error) => {
return false
});
}
// 修改数据的请求函数
export const editRecord = async(values: IFormProps, id: number) => {
return extendRequest('/api/users/'+id, {
method: 'put',
data: values
})
.then((response) => {
return true
})
.catch((error) => {
return false
});
}
// 添加数据的请求函数
export const addRecord = async(values: IFormProps, id?: number) => {
return extendRequest('/api/users', {
method: 'post',
data: values
})
.then((response) => {
return true
})
.catch((error) => {
return false
});
}
- src/models/data.d.ts 用于定义不同文件之间使用的接口-interface
export interface ISingleUser {
id: number,
name: string,
email: string,
create_time: string,
update_time: string,
status: number
}
export interface IFormProps {
[name: string]: any
}
- .umirec.ts umi的配置文件。
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
// 配置代理
proxy: {
'/api': {
'target': 'http://public-api-v1.aspirantzhang.com',
'changeOrigin': true,
'pathRewrite': { '^/api' : '' },
},
},
});
文件中没有配置 routes
项,umi 会使用约定路由 --- 通过 localhost:8000/users 访问。