@complex-suite/request
Advanced tools
+11
-0
@@ -10,2 +10,13 @@ <!-- | ||
| ### 2.0.16 | ||
| - fix(types): 新增 `RequestConfigWithUrl<R,L>` 类型(url必填其余可选),`request`/`get`/`post`/`form`/`json` 五方法入参从 `Partial<RequestConfig>` 改为 `RequestConfigWithUrl`,编译时强制 url 必传 | ||
| - fix(request): `_parseRequestConfig` 开头对 data 做 `deepCloneData` 深拷贝,隔离调用方原对象与内部修改(类型转换、jsonToForm 等) | ||
| - fix(request): `_handleTokenLogin` 复用 `isLogining` 合并并发登录,若 !this.isLogining 则赋值 `this.rule.login('token')`,所有等待者共享同一 Promise,完成后在 finally 清空,与 `_handleRequest` 中的 login/refresh 分支逻辑一致 | ||
| - fix(types): `RuleInitOption.refresh` 从必填改为可选 `refresh?: refreshType`,与运行时 `if (this.rule.refresh && ...)` 判空逻辑一致 | ||
| - refactor(rule): 移除 `Rule.ts` 中 `defaultFormatUrl` 函数、`formatUrl` 字段及赋值、`formatUrlType` 导入,移除 `RuleInitOption.formatUrl` 字段,这些为从未被读取的死代码 | ||
| - fix(auth): `_handleTokenLogin` 和 `_handleRequest` 的 refresh/login 分支引入 `refreshCount`/`loginCount` 引用计数器(声明处 `= 0` 初始化以满足 `strictPropertyInitialization`),创建共享 Promise 时重置为 0,加入时递增,重试 `.finally()` 和共享 Promise `.catch()` 中递减,仅当计数归零时才清空 `isRefreshing`/`isLogining`,避免首个重试完成即清空标志导致 straggler 触发冗余 refresh/login | ||
| - fix(auth): `_handleRequest` refresh 分支第239行条件追加 `&& trigger !== 'login'`,trigger='login' 时跳过 refresh 块和 login 块落到 else reject,终止 refresh+login 都失败后的异常无限循环 | ||
| - fix(auth): `_request` token 缺失分支增加 `trigger === 'login'` 守卫,login 后 token 仍缺失时直接 `$exportMsg` + `_showFail` + `Promise.reject` 终止,避免再次调用 `_handleTokenLogin` 形成无限微任务循环 | ||
| - chore(BaseRequest): 删除未使用的 `messageType` 类型导入 | ||
| ### 2.0.15 | ||
@@ -12,0 +23,0 @@ - fix(auth): `_handleRequest` login 分支增加 `if (trigger !== 'login')` 守卫,else reject(finalResponse),与 refresh 分支二级降级(refresh→login→reject)一致,避免登录后重试再返回 login 状态时无限循环。 |
+3
-3
| { | ||
| "name": "@complex-suite/request", | ||
| "version": "2.0.15", | ||
| "version": "2.0.16", | ||
| "description": "a complex request", | ||
@@ -15,4 +15,4 @@ "type": "module", | ||
| "dependencies": { | ||
| "@complex-suite/utils": "3.0.15", | ||
| "@complex-suite/plugin": "5.0.15" | ||
| "@complex-suite/plugin": "5.0.16", | ||
| "@complex-suite/utils": "3.0.16" | ||
| }, | ||
@@ -19,0 +19,0 @@ "devDependencies": { |
+72
-19
@@ -1,8 +0,7 @@ | ||
| import { _Data, jsonToForm } from "@complex-suite/utils" | ||
| import { _Data, jsonToForm, deepCloneData } from "@complex-suite/utils" | ||
| import { notice } from "@complex-suite/plugin" | ||
| import type { messageType } from "@complex-suite/plugin" | ||
| import Rule from "./Rule" | ||
| import { requestLocale } from "./locale" | ||
| import type { RequestMessages } from "./locale" | ||
| import type { formatUrlType, RequestInitOption, methodType, failType, totalFailType, failOption, requestTrigger, RequestConfig, responseType } from "./types" | ||
| import type { formatUrlType, RequestInitOption, methodType, failType, totalFailType, failOption, requestTrigger, RequestConfig, RequestConfigWithUrl, responseType } from "./types" | ||
@@ -13,3 +12,3 @@ type statusType = { | ||
| export type { formatUrlType, RequestInitOption, methodType, failType, totalFailType, failOption, requestTrigger, RequestConfig } | ||
| export type { formatUrlType, RequestInitOption, methodType, failType, totalFailType, failOption, requestTrigger, RequestConfig, RequestConfigWithUrl } | ||
@@ -89,2 +88,4 @@ const defaultFormatUrlWithBaseUrl = function(this: BaseRequest, url: string) { | ||
| isRefreshing?: Promise<any> | ||
| refreshCount: number = 0 | ||
| loginCount: number = 0 | ||
| status: statusType | ||
@@ -126,2 +127,6 @@ formatUrl: formatUrlType | ||
| } | ||
| // 深拷贝 data,隔离调用方与内部对 data 的修改(类型转换、append 等) | ||
| if (requestConfig.data) { | ||
| requestConfig.data = deepCloneData(requestConfig.data) | ||
| } | ||
| const targetType = requestConfig.targetType || 'json' | ||
@@ -169,3 +174,3 @@ if (requestConfig.currentType == undefined) { | ||
| } | ||
| request(requestConfig: Partial<RequestConfig<R, L>>) { | ||
| request(requestConfig: RequestConfigWithUrl<R, L>) { | ||
| // 浅拷贝入参,避免 _parseRequestConfig 修改 method/url/currentType 等污染调用方原对象 | ||
@@ -193,2 +198,9 @@ return this._request(this._parseRequestConfig({ ...requestConfig })) | ||
| if (res.token) { | ||
| if (trigger === 'login') { | ||
| // login 后 token 仍缺失,说明 login 实现有问题,直接 reject 避免无限递归 | ||
| const msg = requestLocale.t('error.tokenAbsent', { prop: res.prop }) | ||
| this.$exportMsg(msg) | ||
| this._showFail(requestConfig.fail, 'token', msg) | ||
| return Promise.reject({ status: 'fail', code: 'token absent after login' }) | ||
| } | ||
| // 存在Token规则但是不存在值,需要调用login接口 | ||
@@ -208,5 +220,22 @@ // 此处不应判断是否为重复操作 | ||
| return new Promise((resolve, reject) => { | ||
| this.rule.login('token').then(() => { | ||
| this._request(requestConfig, 'login').then(resolve).catch(reject) | ||
| }).catch(reject) | ||
| // 复用 isLogining 合并并发登录,避免多个请求各自触发 rule.login('token') | ||
| if (!this.isLogining) { | ||
| this.isLogining = this.rule.login('token') | ||
| this.loginCount = 0 | ||
| } | ||
| this.loginCount++ | ||
| this.isLogining.then(() => { | ||
| this._request(requestConfig, 'login').then(resolve).catch(reject).finally(() => { | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| }) | ||
| }).catch((err) => { | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| reject(err) | ||
| }) | ||
| }) | ||
@@ -222,3 +251,3 @@ } | ||
| } else if (finalResponse.status === 'refresh') { | ||
| if (this.rule.refresh && trigger !== 'refresh') { | ||
| if (this.rule.refresh && trigger !== 'refresh' && trigger !== 'login') { | ||
| // 接口返回 refresh:token 过期但可刷新。首次调用 rule.refresh() 刷新 token | ||
@@ -228,10 +257,18 @@ // 刷新成功后以 trigger='refresh' 重试请求;刷新失败则直接 reject | ||
| this.isRefreshing = this.rule.refresh() | ||
| this.refreshCount = 0 | ||
| } | ||
| this.refreshCount++ | ||
| this.isRefreshing.then(() => { | ||
| this._request(requestConfig, 'refresh').then(resolve).catch(reject).finally(() => { | ||
| this.isRefreshing = undefined | ||
| this.refreshCount-- | ||
| if (this.refreshCount === 0) { | ||
| this.isRefreshing = undefined | ||
| } | ||
| }) | ||
| }).catch((err) => { | ||
| // refresh 本身失败时也需清空 isRefreshing,避免后续请求复用 rejected Promise 永久卡死 | ||
| this.isRefreshing = undefined | ||
| this.refreshCount-- | ||
| if (this.refreshCount === 0) { | ||
| this.isRefreshing = undefined | ||
| } | ||
| reject(err) | ||
@@ -244,10 +281,18 @@ }) | ||
| this.isLogining = this.rule.login('refresh') | ||
| this.loginCount = 0 | ||
| } | ||
| this.loginCount++ | ||
| this.isLogining.then(() => { | ||
| this._request(requestConfig, 'login').then(resolve).catch(reject).finally(() => { | ||
| this.isLogining = undefined | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| }) | ||
| }).catch((err) => { | ||
| // login 失败时清空 isLogining,避免后续请求复用 rejected Promise 永久卡死 | ||
| this.isLogining = undefined | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| reject(err) | ||
@@ -264,10 +309,18 @@ }) | ||
| this.isLogining = this.rule.login('login') | ||
| this.loginCount = 0 | ||
| } | ||
| this.loginCount++ | ||
| this.isLogining.then(() => { | ||
| this._request(requestConfig, 'login').then(resolve).catch(reject).finally(() => { | ||
| this.isLogining = undefined | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| }) | ||
| }).catch((err) => { | ||
| // login 失败时清空 isLogining,避免后续请求复用 rejected Promise 永久卡死 | ||
| this.isLogining = undefined | ||
| this.loginCount-- | ||
| if (this.loginCount === 0) { | ||
| this.isLogining = undefined | ||
| } | ||
| reject(err) | ||
@@ -299,15 +352,15 @@ }) | ||
| abstract $parseError(responseError: unknown): { msg?: string, type: failType, data: unknown } | ||
| get(requestConfig: Partial<RequestConfig<R, L>>) { | ||
| get(requestConfig: RequestConfigWithUrl<R, L>) { | ||
| const config = { ...requestConfig, method: 'get' as const } | ||
| return this.request(config) | ||
| } | ||
| post(requestConfig: Partial<RequestConfig<R, L>>) { | ||
| post(requestConfig: RequestConfigWithUrl<R, L>) { | ||
| const config = { ...requestConfig, method: 'post' as const } | ||
| return this.request(config) | ||
| } | ||
| form(requestConfig: Partial<RequestConfig<R, L>>) { | ||
| form(requestConfig: RequestConfigWithUrl<R, L>) { | ||
| const config = { ...requestConfig, method: 'post' as const, currentType: 'form' as const, targetType: 'form' as const } | ||
| return this.request(config) | ||
| } | ||
| json(requestConfig: Partial<RequestConfig<R, L>>) { | ||
| json(requestConfig: RequestConfigWithUrl<R, L>) { | ||
| // 注意: 此方法并非直接以 application/json 发送数据,而是将 json 对象转换为 FormData 发送。 | ||
@@ -314,0 +367,0 @@ // currentType='json' 标识当前 data 为 json 对象,targetType='form' 触发 _parseRequestConfig 中的 jsonToForm 转换。 |
+2
-8
@@ -5,3 +5,3 @@ import { _Data } from "@complex-suite/utils" | ||
| import { requestLocale } from "./locale" | ||
| import type { tokenType, responseType, RuleInitOption, formatUrlType } from "./types" | ||
| import type { tokenType, responseType, RuleInitOption } from "./types" | ||
@@ -15,6 +15,2 @@ export type { tokenType, responseType, RuleInitOption } | ||
| function defaultFormatUrl(url: string) { | ||
| return url | ||
| } | ||
| class Rule<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> extends _Data{ | ||
@@ -29,4 +25,3 @@ static $name = 'Rule' | ||
| login: loginType | ||
| refresh: refreshType | ||
| formatUrl: formatUrlType | ||
| refresh?: refreshType | ||
| constructor(initOption: RuleInitOption<R, L>) { | ||
@@ -50,3 +45,2 @@ super() | ||
| this.refresh = initOption.refresh | ||
| this.formatUrl = initOption.formatUrl || defaultFormatUrl | ||
| } | ||
@@ -53,0 +47,0 @@ $appendToken(requestConfig: RequestConfig<R, L>) { |
@@ -54,4 +54,3 @@ // request 包类型集中管理 | ||
| login: loginType // 登录操作,触发于token本地验证失败时\接口login\接口refresh成功后重新调用依然需要refresh时 | ||
| refresh: refreshType // 刷新操作,触发于请求提示refresh时 | ||
| formatUrl?: formatUrlType // 格式化对应URL | ||
| refresh?: refreshType // 刷新操作,触发于请求提示refresh时 | ||
| } | ||
@@ -102,1 +101,4 @@ | ||
| } | ||
| /** url 必传的请求配置类型:url 为必填,其余字段均为可选 */ | ||
| export type RequestConfigWithUrl<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> = Partial<Omit<RequestConfig<R, L>, 'url'>> & { url: string } |
56624
7.84%950
5.44%+ Added
+ Added
- Removed
- Removed
Updated
Updated