返回文章列表
/更新于

开发原则:按需调用接口,及时反馈状态

好的产品实现不应该一上来就把所有重数据接口都打满,也不应该让用户操作后停在无反馈状态。接口调用要跟随用户真实交互,UI 状态要清楚表达进行中、成功和失败。

  • Product
  • Engineering
  • UX
  • Frontend
  • React

有些开发原则看起来很小,但会直接影响一个产品用起来是轻还是重、是可信还是让人不安。

我最近会特别强调两个原则:

  • 按需调用接口,不要一上来就请求重数据
  • UI 操作后必须给用户反馈

它们一个影响系统负载和页面响应,一个影响用户对操作结果的判断。

一、接口调用要跟着用户交互走

很多页面一打开,就把所有可能用到的数据都请求回来。

这种做法看起来省事,问题也很明显:

  • 首屏变慢
  • 后端压力变大
  • 很多数据其实用户根本不会看到
  • 页面逻辑更难判断哪些数据是真正必要的
  • 一旦某个重接口失败,可能拖累整个页面体验

更好的默认策略是:

用户交互到哪里,接口再按需要触发到哪里。

比如一个页面里有多个详情面板、统计图、历史记录和高级配置,不一定要在页面初始化时全部请求。

更合理的方式通常是:

  • 首屏只加载用户马上需要看的核心数据
  • 用户展开详情时,再请求详情数据
  • 用户切换到某个 tab 时,再请求这个 tab 对应的数据
  • 用户触发分析、生成、导出这类重操作时,再调用对应接口
  • 已经请求过且仍然有效的数据,可以按策略缓存

这样做不是为了少写代码,而是为了让系统行为更接近真实使用路径。

用户没有进入某个区域,就不应该让这个区域提前消耗大量资源。

尤其是重数据接口、长耗时接口、付费资源接口、会触发复杂计算的接口,更不应该在页面加载时无差别调用。

这里的判断标准很简单:

接口调用应该服务于当前用户意图,而不是服务于开发时的省事。

如果一个数据不是首屏决策所必需,也不是当前交互马上要展示的结果,就应该先问一句:它是否真的需要现在请求?

二、UI 操作后要给出反馈

另一个很容易被忽略的问题,是用户点了按钮、提交了表单、触发了任务,但界面没有任何明确反馈。

这会让用户不知道:

  • 点击是否生效
  • 请求是否正在进行
  • 操作是否已经成功
  • 失败后应该怎么处理
  • 能不能重复点击

所以任何会产生状态变化的 UI 操作,都应该给出反馈。

最基本的反馈包括三类:

  • 进行中
  • 成功
  • 失败

进行中状态要告诉用户操作已经被接收,并且系统正在处理。

例如:

  • 按钮进入 loading
  • 表单提交期间禁用重复提交
  • 长任务显示进度或当前阶段
  • 后台任务提示“已开始处理”

成功状态要告诉用户结果已经生效。

例如:

  • 保存成功
  • 已复制
  • 已提交
  • 已创建任务
  • 已更新配置

失败状态要告诉用户哪里失败了,以及下一步能做什么。

例如:

  • 网络异常,请稍后重试
  • 参数不完整,请补充必填项
  • 权限不足,请联系管理员
  • 任务启动失败,可以重新提交

这里最差的体验不是失败,而是失败了却没有反馈。

因为没有反馈时,用户只能猜。

他可能会反复点击,可能会刷新页面,可能会以为系统卡死,也可能会在不知道结果的情况下继续做后续操作。

所以 UI 状态不是装饰,它是产品行为的一部分。

一个操作是否可信,不只取决于后端有没有成功处理,也取决于前端有没有把处理状态清楚表达出来。

三、React 里的数据请求要有默认护栏

如果页面是 React 实现,按需调用接口还要进一步处理 Effect、竞态和缓存问题。

React 文档里有几个很关键的建议。

第一,Effect 里请求数据时,要有 cleanup 或 ignore 机制。

因为依赖变化、组件卸载、用户快速切换条件时,旧请求可能比新请求更晚返回。如果旧请求返回后还继续 setState,页面就可能把过期数据写回当前状态。

React 文档里的典型做法是在 Effect 内部放一个 ignore 标记,并在 cleanup 里把它改掉:

useEffect(() => {
  let ignore = false;

  async function startFetching() {
    const result = await fetchData(requestKey);
    if (!ignore) {
      setData(result);
    }
  }

  startFetching();

  return () => {
    ignore = true;
  };
}, [requestKey]);

这个写法的重点不是 ignore 这个变量本身,而是原则:

旧请求不能回写新状态。

第二,开发环境的 Strict Mode 会故意执行 setup -> cleanup -> setup

这意味着 Effect 逻辑必须能承受重复挂载、重复清理、重复请求。

不要假设“组件只会挂载一次”,也不要靠“开发环境多请求一次没关系”来掩盖问题。React 这么做,是为了让缺失 cleanup、重复订阅、资源泄漏、竞态回写这些问题更早暴露出来。

第三,不要手写一堆分散的 Effect 请求逻辑。

如果每个组件都自己写 useEffect + fetch + loading + error + ignore + cache,最后很容易变成:

  • 请求逻辑重复
  • loading 和 error 表达不一致
  • 缓存策略不一致
  • 同一个接口被多个组件重复请求
  • 竞态问题散落在各处

React 文档更推荐优先使用框架的数据加载能力,或者使用、构建 client-side cache。

例如:

  • React Router loader
  • TanStack Query
  • SWR
  • 框架内置的数据加载机制

这些方案通常会统一处理缓存、去重、预加载、请求状态和竞态问题,比在每个组件里散写 Effect 更稳。

第四,如果自己实现缓存,要按 request key 去重,并复用 in-flight promise。

同一个请求 key 如果已经在请求中,就不应该再打一次网络,而应该复用当前还没完成的 promise。React Suspense 文档里的缓存示例,本质也是用 Map<url, promise> 这类结构来记录请求:

const cache = new Map<string, Promise<unknown>>();

function fetchWithCache(key: string) {
  if (!cache.has(key)) {
    cache.set(key, fetchData(key));
  }

  return cache.get(key);
}

真实项目里,这个 key 不能只随便拼一个 URL,而应该能稳定表达一次请求的身份。

例如:

  • endpoint
  • query 参数
  • 当前用户或租户
  • 分页参数
  • filter 和 sort

这样才能避免两个问题:

  • 同一个请求被重复触发,浪费网络和计算资源
  • 不同请求被错误复用,导致数据显示串了

所以 React 页面里的接口调用,不只是“什么时候请求”的问题,还包括:

  • 旧请求能不能被安全忽略
  • 重复 Effect 会不会破坏状态
  • 请求状态是否集中管理
  • 相同 request key 是否能复用 in-flight promise

这几件事处理好了,按需调用才不会变成一堆脆弱的局部 Effect。

四、这两个原则其实是一件事

按需调用接口,解决的是系统不要提前做不必要的事。

操作后给出反馈,解决的是系统做了事之后要让用户知道。

前者要求开发者尊重用户真实路径。

后者要求开发者尊重用户当前感受。

放在一起看,它们都指向同一个原则:

系统行为要被用户意图驱动,也要被用户感知到。

如果页面还没交互,就把所有接口都打满,这是系统抢跑。

如果用户已经操作了,界面却没有状态反馈,这是系统失语。

好的产品实现应该避免这两件事。

它应该在用户需要时再行动,并且在行动后及时告诉用户发生了什么。