@skyroc/service
平台无关的请求与查询基础设施,通过适配器模式将业务状态码管理、Token 自动刷新、错误消息去重、QueryClient 配置等能力跨端复用
概述
@skyroc/service 是 @skyroc/axios 和 @tanstack/react-query 的上层封装,采用适配器(Adapter)模式将平台差异(UI 反馈、认证存储、路由导航、国际化)从核心逻辑中分离:
createAppRequest:创建请求实例,内置业务状态码驱动的错误处理、Token 自动刷新与重试、错误消息去重createQueryClient:创建预配置的 TanStack QueryQueryClient,提供合理的缓存与重试默认值- 零平台假设:antd、Material UI、React Native、Next.js 等均可通过实现
RequestAdapter接口接入
架构
┌─────────────────────────────────────────────────────────┐
│ 业务层 (React 组件 / hooks) │
│ useQuery / useMutation / request(...) │
└──────────────────────┬──────────────────────────────────┘
│
┌─────────────▼────────────────┐
│ @skyroc/service │
│ │
│ createAppRequest │
│ ├─ isBackendSuccess │
│ ├─ onRequest (注入 Token) │
│ ├─ onBackendFail │
│ │ ├─ logout 码 → 登出 │
│ │ ├─ modalLogout → 弹窗 │
│ │ └─ expiredToken → 刷新│
│ └─ onError (去重展示) │
│ │
│ createQueryClient │
│ ├─ DEFAULT_QUERY_CONFIG │
│ └─ DEFAULT_MUTATION_CONFIG│
└──────────────┬───────────────┘
│
┌──────────────▼───────────────┐
│ @skyroc/axios │
│ createRequest / Axios │
└──────────────────────────────┘快速上手
1. 实现平台适配器
import type { RequestAdapter } from '@skyroc/service';
const adapter: RequestAdapter = {
getToken: () => localStorage.getItem('token'),
getRefreshToken: () => localStorage.getItem('refreshToken'),
setAuth: ({ token, refreshToken }) => {
localStorage.setItem('token', token);
localStorage.setItem('refreshToken', refreshToken);
},
resetAuth: () => {
localStorage.removeItem('token');
localStorage.removeItem('refreshToken');
},
getCurrentPath: () => window.location.pathname,
redirectToLogin: (redirectPath) => {
window.location.href = `/login?redirect=${redirectPath}`;
},
showErrorMessage: (msg, onClose) => {
message.error({ content: msg, onClose });
},
showErrorModal: ({ title, content, onConfirm, maskClosable }) => {
Modal.error({ title, content, onOk: onConfirm, maskClosable });
},
t: (key) => i18n.t(key),
fetchRefreshToken: async (refreshToken) => {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
body: JSON.stringify({ refreshToken }),
headers: { 'Content-Type': 'application/json' },
});
return res.json();
},
};2. 创建请求实例
import { createAppRequest } from '@skyroc/service';
export const request = createAppRequest({
adapter,
codes: {
success: '0000',
logout: ['8888'],
modalLogout: ['7777'],
expiredToken: ['9999'],
},
axiosConfig: {
baseURL: import.meta.env.VITE_API_BASE_URL,
},
});3. 创建 QueryClient
import { createQueryClient } from '@skyroc/service/query';
export const queryClient = createQueryClient({
queryCache: {
onError: (error) => console.error('Query error:', error),
},
});4. 在组件中使用
import { QueryClientProvider } from '@tanstack/react-query';
const App = () => (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
// API 定义
function fetchUsers(params: Api.UserListParams) {
return request<Api.UserList>({ url: '/api/users', params });
}
// Hook
function useUsers(params: Api.UserListParams) {
return useQuery({
queryKey: ['users', params],
queryFn: () => fetchUsers(params),
});
}API
createAppRequest(options)
创建一个平台无关的请求实例,内置完整的错误处理和 Token 管理。
function createAppRequest(options: CreateRequestOptions): RequestInstance参数:
interface CreateRequestOptions {
/** 平台适配器 */
adapter: RequestAdapter;
/** Axios 基础配置 */
axiosConfig?: CreateAxiosDefaults;
/** 后端业务状态码 */
codes: ServiceCodes;
/** 自定义后端成功判断(默认:String(response.data.code) === codes.success) */
isBackendSuccess?: (response: { data: { code: string | number } }) => boolean;
/** 自定义响应数据转换(默认:response.data.data) */
transform?: (response: any) => any;
}返回值:
返回一个 RequestInstance,用法与 @skyroc/axios 的 createRequest 一致,额外提供:
| 属性 / 方法 | 说明 |
|---|---|
request(config) | 发送请求,返回转换后的业务数据 |
request.cancelAllRequest() | 取消所有进行中的请求 |
request.state | 实例内部状态(errMsgStack、refreshTokenPromise) |
createQueryClient(options?)
创建一个预配置的 TanStack Query QueryClient,合并内置默认值与自定义配置。
function createQueryClient(options?: CreateQueryClientOptions): QueryClient参数:
interface CreateQueryClientOptions {
/** 覆盖默认 defaultOptions(会与内置默认值浅合并) */
defaultOptions?: DefaultOptions;
/** MutationCache 配置(onError / onSuccess / onSettled / onMutate) */
mutationCache?: MutationCacheConfig;
/** QueryCache 配置(onError / onSuccess / onSettled) */
queryCache?: QueryCacheConfig;
}核心接口
RequestAdapter
平台适配器接口,不同平台(antd / Material UI / RN / Next.js)实现此接口即可接入。
| 方法 | 说明 |
|---|---|
getToken() | 获取 access token |
getRefreshToken() | 获取 refresh token |
setAuth(tokens) | 保存认证信息 { token, refreshToken } |
resetAuth() | 清除认证信息 |
getCurrentPath() | 获取当前路由路径 |
redirectToLogin(path?) | 重定向到登录页 |
showErrorMessage(msg, onClose?) | 展示错误消息(toast / message) |
showErrorModal(options) | 展示错误弹窗(modal / dialog) |
t(key) | 国际化翻译 |
fetchRefreshToken(refreshToken) | 使用 refresh token 换取新 token |
ServiceCodes
后端业务状态码配置,不同环境 / 后端可能使用不同的 code 体系。
interface ServiceCodes {
/** 请求成功的状态码 */
success: string;
/** 需要登出的状态码 */
logout: string[];
/** 需要弹窗确认后登出的状态码 */
modalLogout: string[];
/** Token 过期需要刷新的状态码 */
expiredToken: string[];
}错误处理流程
createAppRequest 内置了一套基于业务状态码的错误处理策略:
后端返回 response
│
▼
isBackendSuccess? ──── Yes ──► transform(response) → 返回数据
│
No
▼
┌─ onBackendFail ─────────────────────────────────────────────┐
│ │
│ logout 码? → showErrorMessage + redirectToLogin │
│ modalLogout 码? → showErrorModal → onConfirm → 登出 │
│ expiredToken 码? → 刷新 Token → 重试原请求 │
│ 其他 → 返回 null → 进入 onError │
│ │
└──────────────────────────────────────────────────────────────┘
│
▼
onError → showErrorMsg(去重展示)登出码
直接展示错误消息并重定向到登录页:
codes: { logout: ['8888'] }
// 后端返回 code: '8888' → 调用 adapter.showErrorMessage + adapter.redirectToLogin弹窗登出码
弹窗确认后登出,同一消息不重复弹窗:
codes: { modalLogout: ['7777'] }
// 后端返回 code: '7777' → 调用 adapter.showErrorModal → 用户确认 → 登出Token 过期码
自动刷新 Token 并重试原请求,并发请求共享同一个刷新 Promise,避免重复刷新:
codes: { expiredToken: ['9999'] }
// 后端返回 code: '9999' → fetchRefreshToken → setAuth → 带新 Token 重试Token 刷新机制
请求 A ─┐ ┌─ 带新 Token 重试 A
请求 B ─┤ 共享同一个 ├─ 带新 Token 重试 B
请求 C ─┤ refreshTokenPromise ├─ 带新 Token 重试 C
└──────────────────────┘
│
1 秒后清除 promise,下次过期重新刷新并发的 Token 过期请求只触发一次 fetchRefreshToken,其余请求等待同一个 Promise 的结果后重试。刷新成功后 1 秒清除缓存的 Promise,避免长时间持有过期引用。
错误消息去重
showErrorMsg 维护一个消息栈,同一条消息在展示期间不会重复弹出:
showErrorMsg("网络异常") → 展示 ✅
showErrorMsg("网络异常") → 跳过(栈中已存在)
↓ 用户关闭消息
showErrorMsg("网络异常") → 展示 ✅(已从栈移除)
↓ 5 秒后
消息栈自动清空默认 Query / Mutation 配置
Query 默认配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
gcTime | 600000(10 分钟) | 垃圾回收时间 |
staleTime | 30000(30 秒) | 数据过期时间 |
retry | 2 | 失败重试次数 |
retryDelay | 指数退避,上限 30 秒 | min(1000 × 2^n, 30000) |
refetchOnMount | true | 组件挂载时重新获取 |
refetchOnReconnect | true | 网络恢复时重新获取 |
refetchOnWindowFocus | false | 窗口聚焦时不重新获取 |
throwOnError | false | 不向上抛出错误 |
networkMode | 'online' | 仅在线时发起请求 |
Mutation 默认配置
| 配置项 | 默认值 | 说明 |
|---|---|---|
gcTime | 60000(1 分钟) | 垃圾回收时间 |
retry | 1 | 失败重试次数 |
retryDelay | 指数退避,上限 10 秒 | min(1000 × 2^n, 10000) |
throwOnError | false | 不向上抛出错误 |
networkMode | 'online' | 仅在线时发起请求 |
可通过 defaultOptions 覆盖任意配置项:
const queryClient = createQueryClient({
defaultOptions: {
queries: { staleTime: 60_000, retry: 3 },
mutations: { retry: 2 },
},
});与 @skyroc/axios 的关系
| 层次 | 职责 |
|---|---|
@skyroc/axios | 请求工厂(拦截器钩子、请求 ID、请求取消、重试) |
@skyroc/service | 业务封装(适配器模式、状态码策略、Token 管理、QueryClient) |
@skyroc/service 调用 @skyroc/axios 的 createRequest,向钩子中注入业务逻辑。日常开发只需引用 @skyroc/service,无需直接操作底层 axios 包。
类型导出
| 类型 | 说明 |
|---|---|
RequestAdapter | 平台适配器接口 |
ServiceCodes | 业务状态码配置 |
RequestInstanceState | 请求实例内部状态(errMsgStack、refreshTokenPromise) |
CreateRequestOptions | createAppRequest 参数类型 |
CreateQueryClientOptions | createQueryClient 参数类型 |
测试
# 从 monorepo 根目录
npx vitest run packages/@core/service/__tests__
# 或在包目录内
cd packages/@core/service && pnpm test
# 含覆盖率报告
pnpm test --coverage测试套件包含 52 个用例,覆盖率:Statements 100% / Branches 95% / Functions 100% / Lines 100%,覆盖:请求实例创建与默认回调、业务状态码(登出 / 弹窗登出 / Token 过期)处理、Token 刷新与并发共享、错误消息去重、QueryClient 配置合并、指数退避重试延迟。