什么是「多个源的搜索结果页面」?

在 B 端系统中,常见是「单个源的搜索结果页面」。其特点为:

  1. 包含 3 个部分:搜索部分(例如表单)、搜索按钮、结果部分(例如表格)。
  2. 只有一个数据来源(我们称之为 API)。
  3. 逻辑是:点击搜索按钮,触发 API,API 执行成功后更新结果部分。
  4. 在 React 组件中,一般会创建 3 个 state:search,result,loading(标识获取数据的状态)。

那么,如果需要对这个 API 进行拆分,就会有「多个源的搜索结果页面」。其特点为:

  1. 依然包含 3 个部分:搜索部分,搜索按钮,结果部分。但结果部分会拆分成两个(Result-1,Result-2)。
  2. 有多个数据来源(API-1,API-2)。
  3. 逻辑是?
  4. React State 是?

二者的对比图如下所示:
image.pngimage.png

方案:单搜索多结果

在这种方案中,我们先设想一下逻辑:

  1. 用户点击了 Search 按钮,同时触发了 API-1 和 API-2,result-1 和 result-2 都处于 loading 状态中。
  2. 这时 API-1 执行完毕,result-1 正常展示结果,result-2 还处于 loading 状态。
  3. 继续运行,API-2 执行完毕,result-2 页可以正常展示结果了。

可以看到逻辑还比较清楚,这种情况下我们需要 5 个 React State,分别是:search,loading-1,result-1,loading-2,result-2。

其中 loading 和 result 是绑定的,我们可以用 hook 封装在一起。

  1. export type UseResultParams<TSearch, TResult> = {
  2. getResult: (search: TSearch) => Promise<TResult>
  3. }
  4. export type UseResultReturns<TSearch, TResult> = {
  5. loading: boolean
  6. result: TResult
  7. getResult: (search: TSearch) => Promise<TResult>
  8. }
  9. export function useResult<TSearch, TResult>(
  10. params: UseResultParams<TSearch, TResult>,
  11. ): UseResultReturns<TSearch, TResult> {
  12. const { getResult: getResultSource } = params
  13. const [loading, setLoading] = useState(false)
  14. const [result, setResult] = useState<TResult>(undefined)
  15. const getResult = useCallback(
  16. async (search: TSearch) => {
  17. try {
  18. setLoading(true)
  19. const result = await getResultSource(search)
  20. setResult(result)
  21. return result
  22. } finally {
  23. setLoading(false)
  24. }
  25. },
  26. [getResultSource],
  27. )
  28. return { loading, result, getResult }
  29. }

然后在页面中,我们可以这样使用:

  1. // 伪代码
  2. export const TestPage: FC = () => {
  3. const {
  4. loading: loading1,
  5. result: result1,
  6. getResult: getResult1,
  7. } = useResult()
  8. const {
  9. loading: loading2,
  10. result: result2,
  11. getResult: getResult2,
  12. } = useResult()
  13. return (
  14. <div>
  15. <div>Search Part</div>
  16. <div
  17. onClick={() => {
  18. getResult1()
  19. getResult2()
  20. }}
  21. >
  22. Search Button
  23. </div>
  24. <div loading={loading1}>{result1}</div>
  25. <div loading={loading2}>{result2}</div>
  26. </div>
  27. )
  28. }

优化方案:单搜索多结果 + 独立的搜索按钮

在上一个方案中,有一个问题:点击搜索同时触发了 API-1 和 API-2,如果用户一次只看一部分的数据,就造成了数据请求的浪费。

常见的有 Tabs 组件(参考:https://ant-design.gitee.io/components/tabs-cn/)。先给出示意图:
image.png
根据示意图,我们看看逻辑应该是什么样子的:

  1. 保存当前所选的 Tab。
  2. 点击搜索按钮时,如果当前选择的是 Tab-1,则调用 API-1;如果是 Tab-2,则调用 API-2。
  3. 用户点击 Tab-1 时,触发 API-1;点击 Tab-2 时,触发 API-2。

逻辑也是比较清楚的,也符合用户默认的操作习惯。

我们尝试用伪代码写一下:

  1. // 伪代码
  2. export const TestPage: FC = () => {
  3. const {
  4. loading: loading1,
  5. result: result1,
  6. getResult: getResult1,
  7. } = useResult()
  8. const {
  9. loading: loading2,
  10. result: result2,
  11. getResult: getResult2,
  12. } = useResult()
  13. const [currentTab, setCurrentTab] = useState('1')
  14. // 根据当前的 Tab 执行不同的搜索
  15. const searchByCurrentTab = () => {
  16. if (currentTab === '1') {
  17. getResult1()
  18. } else {
  19. getResult2()
  20. }
  21. }
  22. // 当 Tab 变化时,触发一次搜索
  23. useEffect(() => {
  24. searchByCurrentTab()
  25. }, [currentTab])
  26. // 搜索按钮的点击函数改为 searchByCurrentTab
  27. // 可以给每个 Tab 新增 loading 状态的显示,用户体验更好
  28. return (
  29. <div>
  30. <div>Search Part</div>
  31. <div onClick={searchByCurrentTab}>Search Button</div>
  32. <Tabs accessKey={currentTab} onChange={(v) => setCurrentTab(v)}>
  33. <Tabs.TabPane
  34. key="1"
  35. tab={<span>{loading1 && <LoadingOutlined />}Tab1</span>}
  36. >
  37. <div loading={loading1}>{result1}</div>
  38. </Tabs.TabPane>
  39. <Tabs.TabPane
  40. key="2"
  41. tab={<span>{loading2 && <LoadingOutlined />}Tab2</span>}
  42. >
  43. <div loading={loading2}>{result2}</div>
  44. </Tabs.TabPane>
  45. </Tabs>
  46. </div>
  47. )
  48. }

[END]