学习使用umi和dva,实现数据共享

3,189 阅读3分钟

dva介绍

根据dva官网的说法: dva 首先是一个基于 reduxredux-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。

目录结构

结果展示

案例内容

  1. 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)
  1. 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
  1. 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;
  1. 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
    });
}
  1. 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
}
  1. .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 访问。