使用hooks重构antd pro的想象力(三)我是如何利用hooks干掉redux的

时间:2022-07-22
本文章向大家介绍使用hooks重构antd pro的想象力(三)我是如何利用hooks干掉redux的,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

经过组件化思维的层层分析,我们将antd pro官方demo的分析页伪代码重构至如下:

export function Analysis() {
  const [dashboardAnalysis, setDashboardAnalysis] = useState<AnalysisData>();
  const [loading, setLoading] = useState(true);
  const {visitData, salesData, searchData, salesPieData, offlineData} = dashboardAnalysis;

  useEffect(() => {
    if (!loading) {
      return;
    }
    api().then(res => {
      setDashboardAnalysis(res.data);
    })
  }, [loading]);

  return (
    <GridContent>
      // 四个长相类似的块合并成一个组件处理
      <IntroduceRow visitData={visitData} />
      // 销售额访问量的图表
      <SalesCard salesData={salesData} selectDate={() => setLoading(true)} />
      // 线上热门搜索
      <TopSearch searchData={searchData} />
      // 销售额类别占比
      <ProportionSales salesPieData={salesPieData} />
      // 离线数据图表
      <OfflineData offlineData={offlineData} />
    </GridContent>
  );
}

那么到这里完了吗?

当然没有。我们将思维维度扩大至整个项目。

项目中的大多数页面,首次加载时,都会去请求一个接口。这个操作是一个几乎一样的逻辑片段。因此我们可以利用自定义hooks的思维,将这个逻辑片段抽离出来,封装成为一个自定义hooks useInitial

首先,我们思考一下,这样一个页面首次加载需要请求数据的公共逻辑片段,需要维护什么状态?

第一个:请求的数据结果 设定泛型参数

第二个:表示正在请求的状态 loading

第三个:出现异常时的提示语句

第四个:传入的参数有哪些,如果参数更改,还得重新请求接口

其他的根据实际情况的不同,还会需要新增更多的参数,这里暂时就这四个。

其次,我们思考第二个问题,自定义hooks需要接收哪些参数?

第一个:api请求函数

第二个:api请求函数的参数

第三个:数据的默认值

也就是说,我们要把该接口涉及到的所有逻辑都放在该自定义hooks中统一处理。把不同的元素都作为参数传入即可。

封装结果如下:

import { useState, useEffect } from 'react';

export type APIFunc<T, P> = (params: P) => Promise<T>

/**
 * @param {api} —api.ts件中封装的接口请求方法
 * @param {defaultData} 页面初始化时接口数据的默认值
 * @param {params} 接口所需要的参数  注意,这里请传入接口需要的完整的参数
 * @param {delay} 当该值为true时,接口不请求
 */
export default function useInitial<T, P>(
  api: APIFunc<T, P>, 
  defaultData: T, 
  _params: P, 
  delay?: boolean
) {
  const [store, setStore] = useState({
    params: _params,
    loading: true,
    data: defaultData,
    errMsg: ''
  });
  const {params, loading, data, errMsg} = store;

  useEffect(() => {
    if (!loading || delay) { return; }
    getData(params);
  }, [loading]);

  function getData(params: P) {
    api(params).then(res => {
      // 这里需要注意,要结合自己团队定义的接口规范返回结果
      let data = res || defaultData;
      setStore({
        ...store,
        errMsg: '',
        loading: false,
        data
      })
    }).catch(e => {
      setStore({
        ...store,
        errMsg: e.message,
        loading: false
      })
    });
  }

  function setParams(params: P, refreshing: boolean) {
    setStore({...store, params});
    refreshing && getData(params);
  }

  return {
    loading,
    setLoading: (loading: boolean) => setStore({...store, loading}),
    data,
    errMsg,
    setParams,
  };
}

这样封装之后,使用起来就非常简单。

使用时,只需要一句代码,我们就能够获取到我们想要的数据,状态,对应的操作方法等。

const {
    loading, data, setParams, errMsg, setLoading
  } = useInitial<AnalysisData, null>(fakeChartData, initState, null);

因此,页面组件里的逻辑就变得非常简单,完整可运行的代码如下:

import React from "react";
import { Col, Row } from 'antd';
import { GridContent } from '@ant-design/pro-layout';
import { fakeChartData } from "./service";
import { AnalysisData } from './data.d';
import useInitial from '@/hooks/useInitial';
import { initState } from '@/pages/dashboard/analysis/model';

import IntroduceRow from "./components/IntroduceRow";
import SalesCard from "./components/SalesCard";
import TopSearch from "./components/TopSearch";
import ProportionSales from "./components/ProportionSales";
import OfflineData from "./components/OfflineData";

export default function AnalysisFC() {
  const {
    loading, data, setParams, errMsg, setLoading
  } = useInitial<AnalysisData, null>(fakeChartData, initState, null);
  const {
    visitData, visitData2, salesData, searchData, offlineData, offlineChartData,
    salesTypeData, salesTypeDataOnline, salesTypeDataOffline,
  } = data;
  const salesPieData = {
    all: salesTypeData,
    online: salesTypeDataOnline,
    stores: salesTypeDataOffline
  };

  if (errMsg) {
    // 处理异常逻辑
  }

  return (
    <GridContent>
      <IntroduceRow visitData={visitData} loading={loading} />

      {/* 这里应该调用setParams(xxxx, true),传入参数并且根据新参数重新请求接口,但是由于是demo接口设计不够合理,没有参数,因此直接调用setLoading刷新接口即可, */}
      <SalesCard loading={loading} salesData={salesData} onChange={() => setLoading(true)} />

      <Row gutter={24} type="flex" style={{ marginTop: 24 }}>
        <Col xl={12} lg={24} md={24} sm={24} xs={24}>
          <TopSearch loading={loading} visitData2={visitData2} searchData={searchData} />
        </Col>
        <Col xl={12} lg={24} md={24} sm={24} xs={24}>
          <ProportionSales loading={loading} salesPieData={salesPieData} />
        </Col>
      </Row>

      <OfflineData loading={loading} offlineData={offlineData} offlineChartData={offlineChartData} />
    </GridContent>
  );
}

我们发现,优化之后的代码,在页面组件里,几乎没有冗余的额外逻辑影响阅读。简单干净利落!

整个优化思维基本上就是这样。通过引入hooks,并借助巧妙的组件化思维,我们将复杂的页面一层层变得非常简单。

最后思考一个问题:

先看图:

我们仔细思考Ant Design Pro项目的整个结构。哪些元素是属于共有的?

头部Header模块。左侧导航模块。设置模块。

当页面切换时,我们发现这些模块始终存在。而变化的,仅仅只是中间的页面模块。

那么,我们是不是可以把这些固定的模块统一整合在同一个顶层页面组件App里?

当然可以

export default function App() {
  return (
    // 头部组件
    <Header />

    // 左侧导航
    <NavSide />

    // 设置
    <Setting />

    // 变化的页面组件
    <Pages />
  );
}

而我们还发现,这些组件的数据,其实是相互不干扰的。也就是说,从这个角度来思考,整个项目里,已经没有真正意义上的共享状态了。

那么意味着什么?

意味着,在这样的组织架构下,我们完全可以不再使用dva中的那一套数据逻辑,redux可以不用了,redux-saga可以不用了,甚至useDispatch可以不用了,useSelecotr也可以不用了,仅仅只使用最简单的hooks的方式来维护数据就足够了。

按照这个思路,大家可以动手改造试试看。改造好了,整个项目就重构完了。

本系列文章为原创,欢迎私信我添加白名单转载。