什么是「多个源的搜索结果页面」?
在 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: boolean
result: TResult
getResult: (search: TSearch) => Promise<TResult>
}
export function useResult<TSearch, TResult>(
params: UseResultParams<TSearch, TResult>,
): UseResultReturns<TSearch, TResult> {
const { getResult: getResultSource } = params
const [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>
<div
onClick={() => {
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.TabPane
key="1"
tab={<span>{loading1 && <LoadingOutlined />}Tab1</span>}
>
<div loading={loading1}>{result1}</div>
</Tabs.TabPane>
<Tabs.TabPane
key="2"
tab={<span>{loading2 && <LoadingOutlined />}Tab2</span>}
>
<div loading={loading2}>{result2}</div>
</Tabs.TabPane>
</Tabs>
</div>
)
}
[END]