@skyroc/service
平台无关的请求与查询基础设施,通过 Adapter 注入 UI / Auth / 导航
概览
| 项 | 值 |
|---|---|
| 包名 | @skyroc/service |
| 版本 | 1.0.0 |
| 依赖 | @skyroc/axios、@tanstack/react-query |
| 子入口 | .、./query |
| 测试 | ✅ 5 个测试(含 error-handler.test.ts、shared.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 配置主入口导出
| 类别 | 符号 |
|---|---|
| 工厂 | createAppRequest、createQueryClient |
| 类型 | CreateRequestOptions、CreateQueryClientOptions、RequestAdapter、RequestInstanceState、ServiceCodes |
核心抽象: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 落入 | 行为 |
|---|---|
success | 取 response.data.data 作为业务数据返回 |
logout | resetAuth() → redirectToLogin() |
modalLogout | showErrorModal() 后 resetAuth() → 登录页 |
expiredToken | 调 fetchRefreshToken() 重发原请求 |
| 其它 | 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 / 进程退出。
这正是「适配器解耦」原则的典型应用,详见 架构 · 适配器模式。