@dd-code/jt-axios-tools
v0.0.7
Published
| 项目 | 说明 | |----------|------| | **目标读者** | 前端架构 / 业务开发 | | **文档目的** | 描述 sdk-tools 请求层的设计:对外暴露 **createAxios**,得到类 axios 实例;Token 默认存 **localStorage**,登录失效以**响应体 data.code** 为准,在**成功回调**中执行刷新与重试;用户信息与 Token 通过 **instance.on('changed:userInfo' / '
Readme
Axios 请求层封装设计文档(sdk-tools)
文档说明
| 项目 | 说明 | |----------|------| | 目标读者 | 前端架构 / 业务开发 | | 文档目的 | 描述 sdk-tools 请求层的设计:对外暴露 createAxios,得到类 axios 实例;Token 默认存 localStorage,登录失效以响应体 data.code 为准,在成功回调中执行刷新与重试;用户信息与 Token 通过 instance.on('changed:userInfo' / 'changed:token', ...) 订阅。 | | 建议阅读顺序 | 概述 → 核心 API → 总体设计 → 接口配置与白名单 → 详细设计(认证与刷新逻辑)→ 使用指南 |
目录
1. 概述
1.1 设计目标
构建通用、可扩展的前端 HTTP 请求层:双 Token 认证、登录失效时单 Promise 刷新、刷新后拉取用户信息并事件通知;认证读写可选注入,不传则默认使用 localStorage,便于接入与替换。
1.2 适用场景
- 中大型前后端分离项目
- 需要统一认证、Token 刷新、且希望请求层与具体存储方式解耦的场景
1.3 核心约定
| 维度 | 说明 |
|----------|------|
| 登录失效判定 | 以响应体 data.code 为准(在响应拦截器的成功回调中判断),非 HTTP 状态码;error 回调不处理登录失效。 |
| 认证存储 | createAxios 的 getToken/setToken 等为可选;不传则使用内置 localStorage(key:access_token、refresh_token)。 |
| 刷新与重试 | 单 Promise 方案:多个请求同时命中登录失效时共用一个 refreshPromise,刷新完成后各自重试原请求;重试请求通过内部 innerInstance 发送,不再经过外层对实例的响应拦截(如 return res.data),始终返回完整 AxiosResponse。 |
| 用户信息与 Token 事件 | 刷新成功后按接口配置拉取用户信息,通过 emit('changed:userInfo', userInfo) 派发;同时触发 emit('changed:token', accessToken)。项目通过 http.on('changed:userInfo', cb) 与 http.on('changed:token', cb) 订阅。 |
2. 核心 API:createAxios
2.1 对项目暴露的用法
- 创建:
const http = createAxios(config),得到与 axios 用法一致的实例。 - 在实例上新增拦截器:
http.interceptors.request.use(...)、http.interceptors.response.use(...),与使用 axios 实例相同。 - 订阅用户信息更新:
http.on('changed:userInfo', (userInfo) => { ... })。内部在刷新 Token 成功后会按接口配置拉取用户信息并触发该事件。 - 订阅 Token 更新:
http.on('changed:token', (token) => { ... })。刷新成功后会派发新的 accessToken。 - 订阅登录失效:
http.on('unauthorized', (message?) => { ... })。当不走刷新或刷新失败时触发。 - 发请求:
http.get(...)、http.post(...)等与 axios 一致。
2.2 createAxios 的 config(CreateAxiosConfig)
| 配置项 | 类型 | 必填 | 说明 | |--------|------|------|------| | baseURL、timeout、headers | 同 AxiosRequestConfig | 按需 | 与 axios 一致 | | getToken | () => string | null | 否 | 不传则使用 localStorageAuth.getToken(key: access_token) | | setToken | (token: string) => void | 否 | 不传则使用 localStorageAuth.setToken | | getRefreshToken | () => string | null | 否 | 不传则使用 localStorageAuth.getRefreshToken(key: refresh_token) | | setRefreshToken | (token: string) => void | 否 | 不传则使用 localStorageAuth.setRefreshToken | | clearAuth | () => void | 否 | 不传则使用 localStorageAuth.clearAuth | | onUnauthorized | (message?: string) => void | 否 | 登录失效时调用;也可用 .on('unauthorized', cb) | | loginExpiredCodes | (number | string)[] | 否 | 视为登录失效的业务 code(或 HTTP 状态码),默认 [401] | | noTokenPaths | PathPattern[] | 否 | 不带 Token 的 path,与请求层内置白名单合并 | | skipRefreshPaths | PathPattern[] | 否 | 登录失效时不走刷新、直接登出的 path,与内置白名单合并 |
PathPattern:string(前缀或精确匹配)或 RegExp。
刷新 Token、获取用户信息的接口地址不在 config 里传,由 api.config.ts 统一配置,内部直接读取并调用;requestRefresh(instance, refreshToken)、requestUserInfo(instance) 也在该文件中定义,接收当前 axios 实例。
3. 总体设计
3.1 架构分层
┌─────────────────────────────────┐
│ 业务应用层 │ ← 调用 createAxios 得到的实例:get/post、interceptors、.on('changed:userInfo'/'changed:token'/'unauthorized')
├─────────────────────────────────┤
│ 拦截器层 │ ← 请求拦截:按白名单注入 Token(attachTokenToConfig)
│ │ ← 响应拦截:成功回调中根据 data.code 判断登录失效 → 刷新 → 重试;error 回调透传
├─────────────────────────────────┤
│ 请求核心层 │ ← createAxios:合并白名单、单 Promise 刷新、doRefresh、emit('changed:userInfo'/'changed:token'/'unauthorized')
├─────────────────────────────────┤
│ 接口配置与认证默认实现 │ ← api.config(refreshToken/getUserInfo、requestRefresh/requestUserInfo)、storageAuth(localStorage)
└─────────────────────────────────┘3.2 核心模块与职责
| 模块 | 职责简述 | |----------------|----------| | index.ts | createAxios 实现:创建 axios 实例,合并白名单,挂载请求/响应拦截器;认证未传时使用 localStorageAuth;响应成功回调内根据 data.code 判断登录失效并执行刷新+重试(重试使用不带响应拦截的 innerInstance,始终返回完整 AxiosResponse);error 回调仅 Promise.reject(error)。 | | api.config.ts | apiConfig(refreshToken、getUserInfo 的 method+path)、layerWhitelist(内置 noTokenPaths/skipRefreshPaths)、requestRefresh(instance, refreshToken)、requestUserInfo(instance)。 | | storageAuth.ts | createLocalStorageAuth(options)、localStorageAuth 单例;localStorage 读写,SSR 下安全降级。 | | types.ts | CreateAxiosConfig、CreateAxiosInstance、ApiConfig、WhitelistConfig、PathPattern、CustomAxiosRequestConfig、RefreshTokenResult、LocalStorageAuth 等。 | | matchPath.ts | path 与白名单(PathPattern[])匹配。 | | emitter.ts | 简单事件派发,供 .on/.off 使用。 |
3.3 请求生命周期(简要)
请求发出 → 请求拦截(白名单判断 → 非 noTokenPaths 则 getToken 并 attachTokenToConfig)→ 发请求
→ 响应成功 → 若 data.code 命中 loginExpiredCodes:
→ 若 skipTokenRefresh 或 path 在 skipRefreshPaths → clearAuth + emit('unauthorized') + reject
→ 否则:单 Promise 刷新 → setToken/setRefreshToken → emit('changed:token') → requestUserInfo → emit('changed:userInfo') → 通过 innerInstance 重试原请求并返回
→ 否则 return response
→ 响应失败(error)→ 直接 Promise.reject(error),不处理登录失效4. 接口配置与白名单
4.1 接口配置文件(api.config.ts)
目的:集中配置“刷新 Token”“获取用户信息”的 method + path,并封装 requestRefresh、requestUserInfo,由 createAxios 内部直接调用;项目在创建实例时无需再传这两个接口地址。
内容:
- apiConfig:refreshToken、getUserInfo 的 method 与 path(可按项目实际接口修改)。
- layerWhitelist:请求层内置白名单 noTokenPaths、skipRefreshPaths(如登录、刷新、验证码等),与 createAxios 传入的 noTokenPaths、skipRefreshPaths 合并后生效。
- requestRefresh(instance, refreshToken):调用刷新接口,需传入当前 axios 实例;内部使用 skipTokenRefresh: true,避免刷新接口失败时进入死循环。
- requestUserInfo(instance):调用获取用户信息接口,需传入当前 axios 实例。
4.2 白名单配置
两类白名单(与 §2.2 对应):
| 白名单 | 含义 | 合并规则 | |--------|------|----------| | noTokenPaths | 这些 path 不携带 Token(不调用 attachTokenToConfig) | 最终列表 = layerWhitelist.noTokenPaths + config.noTokenPaths | | skipRefreshPaths | 这些 path 在登录失效时不尝试刷新,直接 clearAuth + emit('unauthorized') | 最终列表 = layerWhitelist.skipRefreshPaths + config.skipRefreshPaths |
单请求标记:某次请求若设置 config.skipTokenRefresh === true,则该请求命中登录失效时也不走刷新,直接 clearAuth + emit('unauthorized'),优先级与白名单一致。
匹配方式:path 支持字符串(前缀或精确)或 RegExp,由 matchPath 统一实现。
5. 详细设计
5.1 请求拦截器
- 职责:按合并后的 noTokenPaths 判断当前请求 url 是否需带 Token;若不需要则跳过,否则 getToken() 并调用 attachTokenToConfig(req, token) 写入 x-access-token、Content-Type、Accept 等。
- 实现:仅此一层,无责任链;项目可在实例上再追加 request/response 拦截器。
5.2 响应拦截器:成功回调中的登录失效与刷新
- 职责:仅在成功回调中根据 response.data.code 判断是否登录失效;error 回调不处理登录失效,直接
Promise.reject(error)。 - 登录失效判定:
loginExpiredCodes包含业务 code(或 HTTP 状态码);当前实现以 data.code 为主(成功响应里才有 data),通过 isLoginExpiredCode(code) 统一比较(支持 number/string)。 - 流程:
- 若
!isLoginExpiredCode(response.data?.code)→ 直接return response。 - 否则取
response.config为 requestConfig,若requestConfig.skipTokenRefresh === true或 url 在 skipRefreshPaths → clearAuth()、fireUnauthorized(...)、Promise.reject(合成错误)。 - 若没有 refreshToken → clearAuth()、fireUnauthorized()、reject。
- 否则:若尚无 refreshPromise,则
refreshPromise = doRefresh().finally(() => { refreshPromise = null; });然后return refreshPromise.then(() => innerInstance.request(requestConfig)),即刷新完成后用原 config 通过 innerInstance 重试一次并返回结果(不会再经过外层对 instance 的响应拦截)。
- 若
5.3 doRefresh:刷新与用户信息与 Token 事件
- 职责:用 getRefreshToken() 取 refreshToken,调用 requestRefresh(instance, refreshToken)(来自 api.config),拿到新 accessToken 后 setToken、可选 setRefreshToken;随后通过 emitter.emit('changed:token', accessToken) 通知 Token 变更;再调用 requestUserInfo(instance),成功后 emitter.emit('changed:userInfo', userInfo);任一步失败则 clearAuth、fireUnauthorized、reject。
- 单 Promise:多个并发请求同时命中登录失效时,共用一个 refreshPromise,各自在
.then(() => innerInstance.request(自己的 config))中重试,不维护队列、不新建多余 Promise。
5.4 事件与 fireUnauthorized
- fireUnauthorized(message?):先
emitter.emit('unauthorized', message),再调用 config.onUnauthorized?.(message)。 - 用户信息事件:仅在 doRefresh 成功拉取用户信息后
emitter.emit('changed:userInfo', userInfo),项目通过 http.on('changed:userInfo', cb) 订阅。 - Token 事件:在 doRefresh 成功刷新 Token 后
emitter.emit('changed:token', accessToken),项目通过 http.on('changed:token', cb) 订阅。
5.5 Token 存储默认实现(storageAuth)
- localStorageAuth:单例,getToken/setToken/getRefreshToken/setRefreshToken/clearAuth 均读写 localStorage(默认 key:access_token、refresh_token);无 window 或 localStorage 时安全降级,不抛错。
- createLocalStorageAuth(options):可自定义 accessTokenKey、refreshTokenKey,返回一组同上方法,便于需要不同 key 或多实例时使用。
5.6 错误回调
- 响应拦截器 error 回调仅执行
Promise.reject(error),不根据 status 或 data.code 做登录失效判断,也不触发刷新。业务/网络错误由项目在自身追加的 response 拦截器或业务层处理。
6. 使用指南
6.1 最简用法(默认 localStorage)
import { createAxios } from 'sdk-tools';
const http = createAxios({
baseURL: import.meta.env.VITE_API_URL ?? '/api',
timeout: 15000,
loginExpiredCodes: [401, 'OVERDUE'],
noTokenPaths: ['/api/open/news'],
skipRefreshPaths: ['/api/auth/logout'],
});
http.on('unauthorized', (message) => {
console.warn('登录失效', message);
// router.push('/login');
});
http.on('changed:userInfo', (userInfo) => {
console.log('用户信息更新', userInfo);
// 存 store
});
const data = await http.get('/users');6.2 自定义 Token 存储(如 Pinia/Vuex)
const http = createAxios({
baseURL: '/api',
getToken: () => useUserStore().token,
setToken: (t) => useUserStore().setToken(t),
getRefreshToken: () => useUserStore().refreshToken,
setRefreshToken: (t) => useUserStore().setRefreshToken(t),
clearAuth: () => useUserStore().logout(),
});6.3 使用 createLocalStorageAuth 自定义 key
import { createAxios, createLocalStorageAuth } from 'sdk-tools';
const auth = createLocalStorageAuth({
accessTokenKey: 'my_access_token',
refreshTokenKey: 'my_refresh_token',
});
const http = createAxios({ baseURL: '/api', ...auth });6.4 单请求不参与刷新
http.post('/api/auth/logout', {}, { skipTokenRefresh: true });6.5 修改刷新/用户信息接口
在 api.config.ts 中修改 apiConfig 的 path(或 requestRefresh/requestUserInfo 内部请求的 url),与项目后端约定一致即可;白名单 layerWhitelist 可一并按需调整。
附录
A. 非浏览器环境
SSR/Node 下无 window.localStorage 时,storageAuth 读写会安全降级(返回 null / 不写入),不抛错;若需在服务端发请求并带 Token,建议传入自定义的 getToken/setToken 等实现。
B. 推荐目录结构(本项目)
src/
utils/requestHttp/
index.ts # createAxios、请求/响应拦截器、attachTokenToConfig、事件挂载
types.ts # CreateAxiosConfig、CreateAxiosInstance、ApiConfig、WhitelistConfig 等
api.config.ts # apiConfig、layerWhitelist、requestRefresh、requestUserInfo
storageAuth.ts # createLocalStorageAuth、localStorageAuth
matchPath.ts # 白名单匹配
emitter.ts # 事件派发
api.ts # 示例:创建 http 实例并导出
index.ts # 包入口,统一导出C. 设计取舍摘要
| 决策 | 原因 | |------|------| | 登录失效以 data.code 为准、且在成功回调中处理 | 与当前项目约定一致(接口常返回 200 + body.code),避免依赖 HTTP 状态码;error 回调仅透传,逻辑清晰。 | | 认证回调可选、默认 localStorage | 降低接入成本;需要时可注入 getToken/setToken 等或使用 createLocalStorageAuth 自定义 key。 | | 单 Promise 刷新 | 多个 401 共用一个 refreshPromise,各自 then 后重试,实现简单、无队列维护。 | | 刷新/用户信息接口在 api.config + requestRefresh(instance) | 接口集中配置;刷新与拉用户信息需用当前 instance(baseURL、拦截器一致),故将 requestRefresh/requestUserInfo 收口到 api.config 并传入 instance。 | | 白名单双配置 | 是否带 Token、是否走刷新由白名单控制;请求层内置与项目传入合并,单请求 skipTokenRefresh 优先。 |
