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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章