使用hooks重新定义antd pro想象力(一)

简介: 本来没计划马上写antd pro,但是有三位大佬打赏了巨额赏金,说能不能讲讲如何在antd pro中使用反应钩子。当然没有问题!没办法,金钱的力量真的伟大[手动狗头]。1react生态中,antd pro占据重要的位置。非常多的团队使用其来完成自己的中后台应用。它的核心数据处理方案dva聚合了react-redux, redux-saga,极大的降低了redux使用的复杂度。因此使用antd pro无疑是一个非常好的方案。

本来没计划马上写antd pro,但是有三位大佬打赏了巨额赏金,说能不能讲讲如何在antd pro中使用react hooks。


当然没有问题!


没办法,金钱的力量真的伟大[手动狗头]。


1


react生态中,antd pro占据重要的位置。非常多的团队使用其来完成自己的中后台应用。它的核心数据处理方案dva聚合了react-redux, redux-saga,极大的降低了redux使用的复杂度。因此使用antd pro无疑是一个非常好的方案。


但是!


在长达一年多的时间里,由于官方并没有针对React hooks提出任何解决方案和推荐方案,因此目前来说,react hooks的开发福利,极少有团队享受到了。


(其实他们内部早就已经在悄悄咪咪的使用了,并且封装了大量简单好用的自定义hooks)


幸运的是,我的团队,早在半年多以前就已经使用react hooks重构了antd pro。开发效率直接飞速提升,简直美滋滋。


不过大家也不用羡慕,后续几篇文章的目的,就是给大家提供一个思路,利用react hooks,去重构antd-pro的demo。


antd pro并非一个入门项目,因此阅读本系列文章,需要有以下基本功底


1.对ant design和antd pro的组件有一定的了解


2.对dva有一定的了解,知道dva的运行机制


3.掌握react的基础知识


4.掌握react hooks基础知识


5.掌握简单的Typescript使用


本系列文章要有最好的阅读效果,建议下载官方的demo,并手动练习更改。当然也可以关注我的项目antd-pro-with-hooks,我重构之后的源码就在这个项目里


2


首先在重构之前,我们需要达成一个共识。


官方提供的demo,在许多实现上,并非最佳方式,因此如果要运用于实践,不可盲从,需要根据实际情况进行调整。


例如:


接口请求的异常处理被封装成公共逻辑,未做差异化处理


/**
 * 配置request请求时的默认参数
 */
const request = extend({
  errorHandler, // 默认错误处理
  credentials: 'include', // 默认请求是否带上cookie
});


工作台页面,某个接口的请求函数。


*fetchUserCurrent(_, { call, put }) {
  const response = yield call(queryCurrent);
  yield put({
    type: 'save',
    payload: {
      currentUser: response,
    },
  });
},


一些地方官方也没有找到合适的实践方案,例如在redux-saga中的类型推导。


微信图片_20220510225320.png


这里因为无法推导出来,返回结果只能显示any。因此实践中我们只能手动指定response的值。


OK,总结一下就是,官方提供的demo中,需要改进的地方还很多,因此,官方demo只能作为参考,切勿作为标准。


接下来,我们就开始来操刀重构!


3


dva中,新的hooks API,useDispatch与useSelector


useDispatchuseSelector是react-redux提供的api。但是集成react-redux的dva一直没有支持他们俩。因此想要使用他们,需要从react-redux中引入


import { useSelector, useDispatch } from 'react-redux';


dva@2.6.0[1]的beta版本也已经支持了这两个api,不过在正式版发布之前,需要指定版本下载依赖。


> yarn add dva@2.6.0-beta.19


useDispatch的作用很简单,就是获取事件触发方法dispatch。它与redux中的dispatch一模一样。


在以往的class语法中,通过connect高阶方法注入组件props的方式获取dispatch方法


@connect
class Center extends PureComponent<CenterProps, CenterState> {}


react hooks直接使用useDispatch获得。hooks的方式来源清清楚楚,更加通俗易懂。


const dispatch = useDispatch();
// 使用时,与之前一样
dispatch({ type: 'dashboardAnalysis/fetch'});


useSelector的使用也非常简单,就是从全局维护的Store状态中,获取当前组件需要的数据。


例如在Demo项目中的分析页,需要获取当前页面对应model维护的状态,直接用useSelector获取即可。


const dashboardAnalysis = useSelector<any, AnalysisData>(state => state.dashboardAnalysis);


那么重构的第一步,就是使用useDispatch + useSelector替换connect。


终于摆脱了connect这种谜一样的写法,浑身都轻松了。


微信图片_20220510225500.png


重构前后对比。


微信图片_20220510225519.jpg


Dashboard的三个页面,分析页,监控页,工作台,都非常简单。以分析页为例,所有的数据都来源于一个接口,只需要在页面组件渲染时请求一次即可。


useEffect(() => {
  dispatch({ type: 'dashboardAnalysis/fetch'});
}, []);


然后相对应的,将组件内部状态改为使用useState维护


const [salesType, setSalesType] = useState<SalesType>('all');
const [currentTabKey, setCurrentTabKey] = useState('');
const [rangePickerValue, setRangePickerValue] = useState(getTimeDistance('year'));


通过useSelector从model中维护的数据获取到。


const dashboardAnalysis = useSelector<any, AnalysisData>(state => state.dashboardAnalysis);
const loadingEffect = useSelector<any, LoadingEffect>(state => state.loading);
const loading = loadingEffect.effects['dashboardAnalysis/fetch'];


到这里,重构的第一步就已经完成了。


4


下一步要思考的问题就是,组件拆分的合理性。


在前面几篇文章里,专门有跟大家分享过,面对一个复杂页面,如何划分组件,如何去确定一个状态所处的位置,那么在官方demo的案例中,使用的方式是否合理?


留下一个思考,下一篇文章分享。


分析页第一步重构之后完整代码,留个备份。


import { Col, Dropdown, Icon, Menu, Row } from 'antd';
import React, { Suspense, useEffect, useState } from 'react';
import { GridContent } from '@ant-design/pro-layout';
import { useSelector, useDispatch } from 'dva';
import PageLoading from './components/PageLoading';
import { getTimeDistance } from './utils/utils';
import { AnalysisData } from './data.d';
import { RangePickerValue } from 'antd/es/date-picker/interface';
import styles from './style.less';
const IntroduceRow = React.lazy(() => import('./components/IntroduceRow'));
const SalesCard = React.lazy(() => import('./components/SalesCard'));
const TopSearch = React.lazy(() => import('./components/TopSearch'));
const ProportionSales = React.lazy(() => import('./components/ProportionSales'));
const OfflineData = React.lazy(() => import('./components/OfflineData'));
export interface LoadingEffect {
  effects: {
    [key: string]: boolean
  },
  global: boolean,
  models: {
    [key: string]: boolean
  }
}
export type SalesType = 'all' | 'online' | 'stores';
export type DateType = 'today' | 'week' | 'month' | 'year';
export default function AnalysisFC() {
  const dashboardAnalysis = useSelector<any, AnalysisData>(state => state.dashboardAnalysis);
  const loadingEffect = useSelector<any, LoadingEffect>(state => state.loading);
  const loading = loadingEffect.effects['dashboardAnalysis/fetch'];
  const dispatch = useDispatch();
  const [salesType, setSalesType] = useState<SalesType>('all');
  const [currentTabKey, setCurrentTabKey] = useState('');
  const [rangePickerValue, setRangePickerValue] = useState(getTimeDistance('year'));
  const {
    visitData, visitData2, salesData, searchData, offlineData, offlineChartData, salesTypeData, salesTypeDataOnline, salesTypeDataOffline,
  } = dashboardAnalysis;
  useEffect(() => {
    dispatch({ type: 'dashboardAnalysis/fetch'});
  }, []);
  const isActive = (type: DateType) => {
    const value = getTimeDistance(type);
    if (!rangePickerValue[0] || !rangePickerValue[1]) {
      return '';
    }
    if (rangePickerValue[0].isSame(value[0], 'day') && rangePickerValue[1].isSame(value[1], 'day')) {
      return styles.currentDate;
    }
    return '';
  }
  const handleRangePickerChange = (rangePickerValue: RangePickerValue) => {
    setRangePickerValue(rangePickerValue)
    dispatch({
      type: 'dashboardAnalysis/fetchSalesData',
    });
  };
  const selectDate = (type: DateType) => {
    setRangePickerValue(getTimeDistance(type));
    dispatch({
      type: 'dashboardAnalysis/fetchSalesData',
    });
  };
  const dropdownGroup = (
    <span className={styles.iconGroup}>
      <Dropdown 
        overlay={
          <Menu>
            <Menu.Item>操作一</Menu.Item>
            <Menu.Item>操作二</Menu.Item>
          </Menu>
        } 
        placement="bottomRight"
      >
        <Icon type="ellipsis" />
      </Dropdown>
    </span>
  );
  const salesPieData = {
    all: salesTypeData,
    online: salesTypeDataOnline,
    stores: salesTypeDataOffline
  }[salesType];
  const activeKey = currentTabKey || (offlineData[0] && offlineData[0].name);
  return (
    <GridContent>
      <Suspense fallback={<PageLoading />}>
        <IntroduceRow loading={loading} visitData={visitData} />
      </Suspense>
      <Suspense fallback={null}>
        <SalesCard
          rangePickerValue={rangePickerValue}
          salesData={salesData}
          isActive={isActive}
          handleRangePickerChange={handleRangePickerChange}
          loading={loading}
          selectDate={selectDate}
        />
      </Suspense>
      <Row gutter={24} type="flex" style={{marginTop: 24,}}>
        <Col xl={12} lg={24} md={24} sm={24} xs={24}>
          <Suspense fallback={null}>
            <TopSearch
              loading={loading}
              visitData2={visitData2}
              searchData={searchData}
              dropdownGroup={dropdownGroup}
            />
          </Suspense>
        </Col>
        <Col xl={12} lg={24} md={24} sm={24} xs={24}>
          <Suspense fallback={null}>
            <ProportionSales
              dropdownGroup={dropdownGroup}
              salesType={salesType}
              loading={loading}
              salesPieData={salesPieData}
              handleChangeSalesType={(e) => setSalesType(e.target.value)}
            />
          </Suspense>
        </Col>
      </Row>
      <Suspense fallback={null}>
        <OfflineData
          activeKey={activeKey}
          loading={loading}
          offlineData={offlineData}
          offlineChartData={offlineChartData}
          handleTabChange={setCurrentTabKey}
        />
      </Suspense>
    </GridContent>
  )
}
相关文章
|
3月前
|
前端开发
React Hooks - useState 的使用方法和注意事项(1),web前端开发前景
React Hooks - useState 的使用方法和注意事项(1),web前端开发前景
|
前端开发
【React工作记录一百零一】再次接触老朋友react+ant design table合并单元格(2)
#yyds干货盘点 再次接触老朋友react+ant design table合并单元格
77 0
|
11月前
|
JavaScript 前端开发 UED
单文件组件(Single-File Components):Vue.js开发的模块化之道
Vue.js是一款流行的JavaScript框架,以其简洁、灵活和易用而广受欢迎。其中一个Vue.js的强大功能就是单文件组件(Single-File Components),它使得Vue.js应用的开发更加模块化和可维护。在本博客中,我们将深入探讨单文件组件的概念、结构、用法,以及如何利用它们来构建清晰、可复用和高效的Vue.js应用。
147 0
|
前端开发 索引
【React工作记录七十九】React+hook+ts+ant design封装一个具有动态表格得页面
【React工作记录七十九】React+hook+ts+ant design封装一个具有动态表格得页面
129 0
|
前端开发 数据格式
【React工作记录一百零一】再次接触老朋友react+ant design table合并单元格(1)
#yyds干货盘点 再次接触老朋友react+ant design table合并单元格
91 0
|
JavaScript 前端开发 API
Vue2 彻底从 Flow 重构为 TypeScript,焕然一新!
事情起源于 4 月 7 号晚上,尤雨溪在推特说,Vue2 收到了一个将整个代码库迁移到 TypeScript 的 PR。
dva和Umi中两个小用法
dva和Umi中两个小用法
187 0
dva创建应用和使用antd中报错解决方案
dva创建应用和使用antd中报错解决方案
83 0
|
前端开发
|
前端开发
#yyds干货盘点 【React工作记录二十一】ant design封装一个弹框组件
#yyds干货盘点 【React工作记录二十一】ant design封装一个弹框组件
171 0
#yyds干货盘点 【React工作记录二十一】ant design封装一个弹框组件