dva.js上手入门

近期在学习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 字段(该字段可自定义),自动帮你处理网络请求的状态,不需要自己再去写 showLoadinghideLoading 方法。

 

下面以一个订单头行的例子来描述一下整个开发过程。

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 函数,并且有 actioneffects 两个参数。第二个参数 effects 包含 putcallselect 三个字段,put 用于触发 actioncall 用于调用异步处理逻辑,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,也算是第一次的探索,先在这里记录一下过程,如果有错误,欢迎指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值