什么是「多个源的搜索结果页面」?
在 B 端系统中,常见是「单个源的搜索结果页面」。其特点为:
- 包含 3 个部分:搜索部分(例如表单)、搜索按钮、结果部分(例如表格)。
- 只有一个数据来源(我们称之为 API)。
- 逻辑是:点击搜索按钮,触发 API,API 执行成功后更新结果部分。
- 在 React 组件中,一般会创建 3 个 state:search,result,loading(标识获取数据的状态)。
那么,如果需要对这个 API 进行拆分,就会有「多个源的搜索结果页面」。其特点为:
- 依然包含 3 个部分:搜索部分,搜索按钮,结果部分。但结果部分会拆分成两个(Result-1,Result-2)。
- 有多个数据来源(API-1,API-2)。
- 逻辑是?
- React State 是?
二者的对比图如下所示:

方案:单搜索多结果
在这种方案中,我们先设想一下逻辑:
- 用户点击了 Search 按钮,同时触发了 API-1 和 API-2,result-1 和 result-2 都处于 loading 状态中。
- 这时 API-1 执行完毕,result-1 正常展示结果,result-2 还处于 loading 状态。
- 继续运行,API-2 执行完毕,result-2 页可以正常展示结果了。
可以看到逻辑还比较清楚,这种情况下我们需要 5 个 React State,分别是:search,loading-1,result-1,loading-2,result-2。
其中 loading 和 result 是绑定的,我们可以用 hook 封装在一起。
export type UseResultParams<TSearch, TResult> = {getResult: (search: TSearch) => Promise<TResult>}export type UseResultReturns<TSearch, TResult> = {loading: booleanresult: TResultgetResult: (search: TSearch) => Promise<TResult>}export function useResult<TSearch, TResult>(params: UseResultParams<TSearch, TResult>,): UseResultReturns<TSearch, TResult> {const { getResult: getResultSource } = paramsconst [loading, setLoading] = useState(false)const [result, setResult] = useState<TResult>(undefined)const getResult = useCallback(async (search: TSearch) => {try {setLoading(true)const result = await getResultSource(search)setResult(result)return result} finally {setLoading(false)}},[getResultSource],)return { loading, result, getResult }}
然后在页面中,我们可以这样使用:
// 伪代码export const TestPage: FC = () => {const {loading: loading1,result: result1,getResult: getResult1,} = useResult()const {loading: loading2,result: result2,getResult: getResult2,} = useResult()return (<div><div>Search Part</div><divonClick={() => {getResult1()getResult2()}}>Search Button</div><div loading={loading1}>{result1}</div><div loading={loading2}>{result2}</div></div>)}
优化方案:单搜索多结果 + 独立的搜索按钮
在上一个方案中,有一个问题:点击搜索同时触发了 API-1 和 API-2,如果用户一次只看一部分的数据,就造成了数据请求的浪费。
常见的有 Tabs 组件(参考:https://ant-design.gitee.io/components/tabs-cn/)。先给出示意图:
根据示意图,我们看看逻辑应该是什么样子的:
- 保存当前所选的 Tab。
- 点击搜索按钮时,如果当前选择的是 Tab-1,则调用 API-1;如果是 Tab-2,则调用 API-2。
- 用户点击 Tab-1 时,触发 API-1;点击 Tab-2 时,触发 API-2。
逻辑也是比较清楚的,也符合用户默认的操作习惯。
我们尝试用伪代码写一下:
// 伪代码export const TestPage: FC = () => {const {loading: loading1,result: result1,getResult: getResult1,} = useResult()const {loading: loading2,result: result2,getResult: getResult2,} = useResult()const [currentTab, setCurrentTab] = useState('1')// 根据当前的 Tab 执行不同的搜索const searchByCurrentTab = () => {if (currentTab === '1') {getResult1()} else {getResult2()}}// 当 Tab 变化时,触发一次搜索useEffect(() => {searchByCurrentTab()}, [currentTab])// 搜索按钮的点击函数改为 searchByCurrentTab// 可以给每个 Tab 新增 loading 状态的显示,用户体验更好return (<div><div>Search Part</div><div onClick={searchByCurrentTab}>Search Button</div><Tabs accessKey={currentTab} onChange={(v) => setCurrentTab(v)}><Tabs.TabPanekey="1"tab={<span>{loading1 && <LoadingOutlined />}Tab1</span>}><div loading={loading1}>{result1}</div></Tabs.TabPane><Tabs.TabPanekey="2"tab={<span>{loading2 && <LoadingOutlined />}Tab2</span>}><div loading={loading2}>{result2}</div></Tabs.TabPane></Tabs></div>)}
[END]
