Skyroc Admin Docs
@core 基础设施

@skyroc/service

平台无关的请求与查询基础设施,通过 Adapter 注入 UI / Auth / 导航

概览

包名@skyroc/service
版本1.0.0
依赖@skyroc/axios@tanstack/react-query
子入口../query
测试✅ 5 个测试(含 error-handler.test.tsshared.test.ts

@skyroc/service 是项目「请求 + 查询」的统一基础设施。它不绑定任何 UI 库 / 路由 / 状态管理实现——所有平台相关的能力通过 RequestAdapter 接口注入,由应用层提供具体实现。

解决了什么问题

底层 @skyroc/axios 只是「类型安全的 HTTP 客户端」,但企业应用还需要:

  • 业务错误码处理(成功 / 登出 / 弹窗登出 / token 过期)
  • token 自动刷新(且并发请求只刷新一次)
  • 国际化错误提示 + 同消息去重
  • 与 TanStack Query 集成(共享 QueryClient、统一缓存策略)

把这些上层模式封装起来,但通过 Adapter 模式避免依赖任何具体 UI 实现,正是 @skyroc/service 的定位。

目录结构

src/
├── index.ts
├── request/
│   ├── create-request.ts   # createAppRequest 工厂
│   ├── error-handler.ts    # 后端业务错误 / 网络错误处理
│   ├── shared.ts           # token 刷新(singleflight)+ 错误消息去重栈
│   └── types.ts            # RequestAdapter、ServiceCodes
└── query/
    ├── create-client.ts    # createQueryClient 工厂
    └── defaults.ts         # 默认 query / mutation 配置

主入口导出

类别符号
工厂createAppRequestcreateQueryClient
类型CreateRequestOptionsCreateQueryClientOptionsRequestAdapterRequestInstanceStateServiceCodes

核心抽象:RequestAdapter

@skyroc/service 通过 RequestAdapter 把所有「平台/应用相关」的能力抽象成接口——任何应用只要实现它,就能把自家的 UI 组件、storage、router 接入请求流水线:

interface RequestAdapter {
  // Auth
  getToken(): string | null;
  getRefreshToken(): string | null;
  setAuth(auth: { token: string; refreshToken: string }): void;
  resetAuth(): void;
  fetchRefreshToken(token: string): Promise<{ data: AuthToken; error: any }>;

  // Navigation
  redirectToLogin(redirect?: string): void;

  // UI
  showErrorMessage(msg: string): void;
  showErrorModal(msg: string): void;

  // i18n
  t(key: string, params?: Record<string, any>): string;
}

应用侧的实现示例见 Admin 请求层

ServiceCodes:业务码 → 行为映射

interface ServiceCodes {
  /** 业务成功码(如 '0000') */
  success: string;
  /** 静默登出(清 storage + 跳登录) */
  logout: string[];
  /** 弹 modal 后登出 */
  modalLogout: string[];
  /** token 过期 → 触发 refresh */
  expiredToken: string[];
}

后端响应的 code 字段会被映射到对应行为:

code 落入行为
successresponse.data.data 作为业务数据返回
logoutresetAuth()redirectToLogin()
modalLogoutshowErrorModal()resetAuth() → 登录页
expiredTokenfetchRefreshToken() 重发原请求
其它showErrorMessage(t('common.requestError', { msg }))

token 刷新:并发请求合并

shared.ts 用 singleflight 模式保证「同一时刻只发起一次 refresh」,并发请求都等同一个 refreshTokenPromise

请求 A 401  ┐
请求 B 401  ├─→ 共用一次 refresh,全部用新 token 重试
请求 C 401  ┘

避免多个请求各自刷新造成 token race。

错误消息去重

同一条错误消息在短时间内只展示一次(栈中检测重复 msg),防止后端短时间返回相同错误时 antd message 堆叠。

使用示例

1. 创建请求实例

import { createAppRequest } from '@skyroc/service';
import { antdAdapter } from './adapter';

export const request = createAppRequest({
  adapter: antdAdapter,
  axiosConfig: {
    baseURL: import.meta.env.VITE_SERVICE_BASE_URL,
    headers: { 'apifoxToken': '...' }
  },
  codes: {
    success: import.meta.env.VITE_SERVICE_SUCCESS_CODE,
    logout: import.meta.env.VITE_SERVICE_LOGOUT_CODES?.split(',') || [],
    modalLogout: import.meta.env.VITE_SERVICE_MODAL_LOGOUT_CODES?.split(',') || [],
    expiredToken: import.meta.env.VITE_SERVICE_EXPIRED_TOKEN_CODES?.split(',') || []
  }
});

2. 创建 QueryClient(子入口)

import { createQueryClient } from '@skyroc/service/query';

export const queryClient = createQueryClient({
  queryCache: { onError: handleError },
  mutationCache: { onError: handleError }
});

默认配置(来自 defaults.ts)已经设定了合理的 staleTime / retry / gcTime,传入的配置会浅合并覆盖。

3. 使用

// 业务接口
export const fetchLogin = (params: Api.Auth.LoginParams) =>
  request<Api.Auth.LoginToken>({ url: '/auth/login', method: 'post', data: params });

// 配合 React Query
import { useQuery } from '@tanstack/react-query';
export const useUserInfoQuery = () =>
  useQuery({ queryKey: ['user'], queryFn: () => fetchGetUserInfo() });

子入口 ./query

按需引入而不引入请求层(如:写一个只需 React Query 的子包):

import { createQueryClient, DEFAULT_QUERY_CONFIG, DEFAULT_MUTATION_CONFIG } from '@skyroc/service/query';

跨端复用

因为 RequestAdapter 是接口而非实现,同一套 @skyroc/service 可以同时服务:

  • Web:注入 antd message / TanStack Router;
  • React Native:注入 RN Toast / Expo Router;
  • Node 脚本:注入 console / 进程退出。

这正是「适配器解耦」原则的典型应用,详见 架构 · 适配器模式

On this page