经过组件化思维的层层分析,我们将antd pro官方demo的分析页伪代码重构至如下:
代码语言:javascript复制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中统一处理。把不同的元素都作为参数传入即可。
封装结果如下:
代码语言:javascript复制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,
};
}
这样封装之后,使用起来就非常简单。
使用时,只需要一句代码,我们就能够获取到我们想要的数据,状态,对应的操作方法等。
代码语言:javascript复制const {
loading, data, setParams, errMsg, setLoading
} = useInitial<AnalysisData, null>(fakeChartData, initState, null);
因此,页面组件里的逻辑就变得非常简单,完整可运行的代码如下:
代码语言:javascript复制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里?
当然可以
代码语言:javascript复制export default function App() {
return (
// 头部组件
<Header />
// 左侧导航
<NavSide />
// 设置
<Setting />
// 变化的页面组件
<Pages />
);
}
而我们还发现,这些组件的数据,其实是相互不干扰的。也就是说,从这个角度来思考,整个项目里,已经没有真正意义上的共享状态了。
那么意味着什么?
意味着,在这样的组织架构下,我们完全可以不再使用dva中的那一套数据逻辑,redux可以不用了,redux-saga可以不用了,甚至useDispatch可以不用了,useSelecotr也可以不用了,仅仅只使用最简单的hooks的方式来维护数据就足够了。
按照这个思路,大家可以动手改造试试看。改造好了,整个项目就重构完了。
本系列文章为原创,欢迎私信我添加白名单转载。