Core Docs

@skyroc/service

平台无关的请求与查询基础设施,通过适配器模式将业务状态码管理、Token 自动刷新、错误消息去重、QueryClient 配置等能力跨端复用

概述

@skyroc/service@skyroc/axios@tanstack/react-query 的上层封装,采用适配器(Adapter)模式将平台差异(UI 反馈、认证存储、路由导航、国际化)从核心逻辑中分离:

  • createAppRequest:创建请求实例,内置业务状态码驱动的错误处理、Token 自动刷新与重试、错误消息去重
  • createQueryClient:创建预配置的 TanStack Query QueryClient,提供合理的缓存与重试默认值
  • 零平台假设: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/axioscreateRequest 一致,额外提供:

属性 / 方法说明
request(config)发送请求,返回转换后的业务数据
request.cancelAllRequest()取消所有进行中的请求
request.state实例内部状态(errMsgStackrefreshTokenPromise

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 默认配置

配置项默认值说明
gcTime600000(10 分钟)垃圾回收时间
staleTime30000(30 秒)数据过期时间
retry2失败重试次数
retryDelay指数退避,上限 30 秒min(1000 × 2^n, 30000)
refetchOnMounttrue组件挂载时重新获取
refetchOnReconnecttrue网络恢复时重新获取
refetchOnWindowFocusfalse窗口聚焦时不重新获取
throwOnErrorfalse不向上抛出错误
networkMode'online'仅在线时发起请求

Mutation 默认配置

配置项默认值说明
gcTime60000(1 分钟)垃圾回收时间
retry1失败重试次数
retryDelay指数退避,上限 10 秒min(1000 × 2^n, 10000)
throwOnErrorfalse不向上抛出错误
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/axioscreateRequest,向钩子中注入业务逻辑。日常开发只需引用 @skyroc/service,无需直接操作底层 axios 包。

类型导出

类型说明
RequestAdapter平台适配器接口
ServiceCodes业务状态码配置
RequestInstanceState请求实例内部状态(errMsgStackrefreshTokenPromise
CreateRequestOptionscreateAppRequest 参数类型
CreateQueryClientOptionscreateQueryClient 参数类型

测试

# 从 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 配置合并、指数退避重试延迟。

On this page