使用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的方式来维护数据就足够了。
按照这个思路,大家可以动手改造试试看。改造好了,整个项目就重构完了。