返回首页

职责:

分类:现代化开发
发布于:
阅读时间:16 分钟
  • queryFn 内部直接调用 setAuth(写副作用)虽然能跑通,但它被认为是一种反模式 (Anti-pattern),因为它违反了“关注点分离”原则。

为什么不建议在 queryFn 里写 setAuth

  1. 幂等性问题queryFn 可能会因为网络重试、窗口聚焦刷新、缓存失效等原因被多次调用。在里面写状态更新会导致不必要的多次 setState

  2. 职责不清queryFn 的唯一职责应该是**“获取并返回数据”。同步 Store、处理重定向属于“响应数据后的副作用”**。

  3. 调试困难:如果你有多个地方用了 useMe,追踪到底是哪次调用触发了 Store 更新会变得非常混乱。

推荐的“工业级”结构化写法

最具有结构性、让人“一眼看懂”的方案是将逻辑拆分为:纯净的数据层全局副作用监听、以及受保护的渲染层

要写出“一眼就能看懂”的工业级代码,理解工具的职责边界是关键。你提到的 QueryClient 正是 TanStack Query(原 React Query)的大脑。

一、 什么是 QueryClient?

简单来说,QueryClient 是一个“智能数据管家”

在没有它之前,你需要手动管理 loadingerrordata 以及“什么时候该重新请求”。有了它之后,它负责维护一个全局的内存缓存(Cache)

  • 它的含义:它是连接你的 React 组件与远程数据的“中间层”。它不关心你的 API 是用 Eden RPC 还是 Axios 写的,它只关心数据是否过期(Stale)、是否需要重构。

  • 形象比喻:它像是一个带缓存的图书馆

    • useQuery 是借书请求。

    • QueryClient 是图书馆管理员。

    • 如果你要的书(数据)在馆内且没过时,管理员直接给你(缓存);如果过时了,管理员去进货(API 请求)。


二、 核心编程规范与职责 (Best Practices)

在现代全栈开发中,遵循**“单一职责原则” (Single Responsibility Principle)** 是最高准则。我们可以把职责划分为四层:

1. 数据层 (The Fetcher) —— 纯净与隔离

  • 职责:只负责与后端打交道,把原始数据拿回来。

  • 规范queryFn 必须是纯函数。不要在里面写 router.push,不要在里面写 setAuth

  • 理由:API 报错或重试不应该直接导致全局状态错乱。

2. 状态层 (The Store) —— 单一事实来源

  • 职责:存储经过加工的、全应用共享的业务状态(如用户信息、当前站点)。

  • 规范:Store 只接收数据并保存,不负责“去哪里拿数据”。

  • 理由:这样你的组件无论从哪个 Hook 拿数据,看到的都是同一份结果。

3. 拦截层 (The Provider/Middleware) —— 副作用中心

  • 职责:监听数据变化,触发后续动作(同步 Store、持久化 LocalStorage、重定向)。

  • 规范:所有的“联动”逻辑(例如:拿到了用户 -> 同步到 Store)应该收拢在这里。


三、 推荐的代码结构规范

为了让你项目中的同事“一眼看懂”,建议采用以下职责分配:

📁 文件组织规范:

  • hooks/api/:放所有的 useQuery。职责:定义“怎么拿数据”。

  • stores/:放 Zustand。职责:定义“数据怎么存”。

  • providers/:放逻辑守卫。职责:定义“数据回来后干什么”。

💡 代码编写模版(职责分明版):

代码职责:获取数据 (API Hook)

TypeScript

// hooks/api/use-user.ts
export const useMe = () => useQuery({
  queryKey: ['me'],
  queryFn: () => rpc.api.v1.me.get().then(handleEden),
  // 规范:只定义获取逻辑和缓存策略
});

代码职责:处理副作用 (Provider)

TypeScript

// providers/user-provider.tsx
export function UserProvider({ children }) {
  const { data, error } = useMe();
  const setAuth = useAuthStore(s => s.setAuth);

  // 规范:使用 useEffect 统一处理“数据到达”后的动作
  useEffect(() => {
    if (data) {
      setAuth(data); // 同步到全局
      console.log("✅ 用户数据已同步至 Store");
    }
  }, [data, setAuth]);

  // 规范:统一处理认证失败
  if (error?.status === 401) return <Redirect to="/login" />;

  return <>{children}</>;
}

四、 总结:为什么要这么麻烦?

如果你把所有逻辑(API + Store + Router)都塞进 queryFn,代码会变成**“不可预测的黑盒”**:

  1. 难以测试:你想测试 API 逻辑,却被迫触发了页面跳转。

  2. 难以重用:另一个组件只想偷偷拿一下数据,结果它一调用,全局 Store 被重置了。

  3. 调试地狱:当 Store 里的数据错了,你不知道是哪次 API 请求触发的副作用。

遵循规范后的感受

  • 当数据没拿到:去看 hooks/api/

  • 当权限按钮没出来:去看 stores/auth-store.ts

  • 当页面没跳转:去看 UserProvider.tsx

这就是所谓的“结构性”——每个文件只讲一个故事。

你想让我针对你目前的权限系统,帮你优化一下那个 PermissionGuard(权限守卫组件)的职责划分吗?这样可以保证即使是复杂的权限,代码依然整洁。