@complex-suite/request 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # Complex Request
2
+
3
+ 这是一个功能强大且高度可扩展的 TypeScript 请求库,旨在通过分层、可组合的规则引擎,优雅地处理复杂的 Web 请求场景。
4
+
5
+ ## 核心特性
6
+
7
+ - **分层架构**:
8
+ - **`BaseRequest`**: 抽象请求基类,负责驱动整个请求生命周期,包括 Token 处理、请求重试、错误捕获等。
9
+ - **`Rule`**: 可定制的规则引擎,将所有与具体业务相关的逻辑(如响应解析、登录/刷新流程)集中管理。
10
+ - **`Token`**: 封装单个 Token 的所有行为,包括值的获取、存储、附加到请求和生命周期管理。
11
+ - **强大的 Token 管理**:
12
+ - 支持多个具名 `Token` 和一个 `refreshToken`。
13
+ - 支持从内存、`localStorage` 和 `sessionStorage` 中自动获取和缓存 Token。
14
+ - 可配置 Token 的有效期、存储位置(`header`, `body`, `params`)和附加键名。
15
+ - 自动化的登录和刷新流程,当 Token 失效时,可自动触发 `login` 或 `refresh` 操作,并无缝地重试原始请求。
16
+ - **高度可定制**:
17
+ - **响应解析**: 可通过 `parse` 函数,将任何格式的服务器响应,适配为统一的 `{ status, data, ... }` 格式。
18
+ - **请求格式化**: 可通过 `format` 函数,在请求发送前,对请求参数进行最后一次修改。
19
+ - **底层实现无关**: `BaseRequest` 是一个抽象类,您可以继承它,并使用任何底层的请求库(如 `axios`, `fetch`)来实现 `$request` 方法。
20
+ - **数据格式转换**:
21
+ - 支持在 `json` 和 `form-data` 之间自动转换请求数据。
22
+
23
+ ## 安装
24
+
25
+ ```bash
26
+ npm install complex-request
27
+ ```
28
+
29
+ ## 快速开始
30
+
31
+ 下面是一个使用 `fetch` 作为底层实现的简单示例:
32
+
33
+ ```typescript
34
+ import { BaseRequest, Rule, Token } from 'complex-request';
35
+
36
+ // 1. 创建一个继承自 BaseRequest 的具体请求类
37
+ // 我们在这里为泛型 R (原始响应) 和 L (本地配置) 提供了具体的类型
38
+ class MyRequest extends BaseRequest<any, { signal?: AbortSignal }> {
39
+ $request(requestConfig) {
40
+ // 使用 fetch 实现底层的请求逻辑
41
+ const { url, method, headers, data, params, local } = requestConfig;
42
+ const finalUrl = new URL(url);
43
+ Object.keys(params).forEach(key => finalUrl.searchParams.append(key, params[key]));
44
+
45
+ return fetch(finalUrl.toString(), {
46
+ method,
47
+ headers,
48
+ body: data,
49
+ signal: local?.signal // 传递 AbortSignal 以便取消请求
50
+ }).then(res => res.json());
51
+ }
52
+
53
+ $parseError(error) {
54
+ // 解析 fetch 抛出的错误
55
+ return { type: 'internal', msg: error.message, data: error };
56
+ }
57
+ }
58
+
59
+ // 2. 定义一套规则
60
+ const myRule = new Rule({
61
+ prop: 'api',
62
+ token: {
63
+ data: {
64
+ // 定义一个名为 'accessToken' 的 Token
65
+ accessToken: {
66
+ location: 'header', // 存储在 Header 中
67
+ require: true // 这是一个必需的 Token
68
+ }
69
+ }
70
+ },
71
+ parse: (response) => {
72
+ // 解析服务器响应
73
+ if (response.code === 200) {
74
+ return { status: 'success', data: response.data };
75
+ } else if (response.code === 401) {
76
+ return { status: 'login', data: null, msg: '请先登录' };
77
+ } else {
78
+ return { status: 'fail', data: null, msg: response.message };
79
+ }
80
+ },
81
+ login: () => {
82
+ // 定义登录逻辑
83
+ return new Promise(resolve => {
84
+ // ... 执行登录操作,例如弹出一个登录框
85
+ // ... 登录成功后,使用 setToken 设置新的 Token
86
+ myRequest.setToken('accessToken', 'new-token-from-server');
87
+ resolve();
88
+ });
89
+ }
90
+ });
91
+
92
+ // 3. 实例化请求对象
93
+ const myRequest = new MyRequest({
94
+ baseUrl: 'https://api.example.com',
95
+ rule: myRule
96
+ });
97
+
98
+ // 4. 发送请求
99
+ // 4. 发送请求,并传递一个本地配置
100
+ const controller = new AbortController();
101
+ myRequest.get({
102
+ url: '/user/profile',
103
+ local: { signal: controller.signal }
104
+ })
105
+ .then(response => {
106
+ console.log('用户信息:', response.data);
107
+ })
108
+ .catch(error => {
109
+ console.error('请求失败:', error);
110
+ });
111
+
112
+ // 可以在需要时取消请求
113
+ // controller.abort();
114
+ ```
115
+
116
+ ## 高级用法:泛型类型
117
+
118
+ `complex-request` 大量使用泛型来提供强大的类型安全。理解 `R` 和 `L` 这两个核心泛型,能帮助您更好地利用这个库。
119
+
120
+ ### `R` - 原始响应 (`Raw Response`)
121
+
122
+ `R` 代表的是底层请求工具(如 `fetch`, `axios`)直接返回的、**未经 `rule.parse` 解析**的原始响应数据的类型。
123
+
124
+ 在 `Rule` 的 `parse` 函数中,`response` 参数的类型就是 `R`。
125
+
126
+ ```typescript
127
+ // 假设服务器返回的数据结构是 { code: number, data: T, message: string }
128
+ type ServerResponse<T> = {
129
+ code: number;
130
+ data: T;
131
+ message: string;
132
+ }
133
+
134
+ // 在定义 Rule 时,我们可以将 R 指定为 ServerResponse<any>
135
+ const myRule = new Rule<ServerResponse<any>>({
136
+ // ...
137
+ parse: (response) => { // 在这里,`response` 的类型就是 ServerResponse<any>
138
+ if (response.code === 200) {
139
+ return { status: 'success', data: response.data };
140
+ }
141
+ // ...
142
+ }
143
+ });
144
+ ```
145
+
146
+ ### `L` - 本地配置 (`Local Config`)
147
+
148
+ `L` 代表的是一个可选的、用于传递给底层请求工具的、**特定于实现的**配置对象的类型。
149
+
150
+ 这非常有用,因为不同的底层请求库(`fetch`, `axios`)有它们自己独特的配置选项(例如,`axios` 的 `cancelToken`,`fetch` 的 `signal`)。`L` 允许您以类型安全的方式,将这些特定于实现的配置,一路传递到您自己实现的 `$request` 方法中。
151
+
152
+ 在上面的“快速开始”示例中,我们通过 `L` 泛型,为 `fetch` 实现了一个请求取消的功能。
153
+
154
+ ### `parse` 函数:请求的“交通警察”
155
+
156
+ `parse` 函数是 `complex-request` 库的**核心**。它就像一个交通警察,负责检查每一个从服务器返回的原始响应 (`R`),并根据您定义的业务逻辑,告诉 `BaseRequest` 下一步应该做什么。
157
+
158
+ `parse` 函数必须返回一个 `responseType` 对象,其 `status` 字段决定了整个请求的走向:
159
+
160
+ ```typescript
161
+ interface responseType<D = any> {
162
+ status: 'success' | 'fail' | 'login' | 'refresh'
163
+ data: D
164
+ code?: number | string
165
+ msg?: string
166
+ }
167
+ ```
168
+
169
+ 下面是 `BaseRequest` 对每种 `status` 的处理方式:
170
+
171
+ - **`'success'`**: **请求成功**
172
+ - **含义**: 服务器成功处理了请求,且响应数据是有效的。
173
+ - **`BaseRequest` 的行为**: `request` 方法返回的 Promise 将被 **resolve**,并将 `data` 字段作为结果传递下去。
174
+
175
+ - **`'fail'`**: **请求失败**
176
+ - **含义**: 服务器返回了一个业务逻辑上的错误(例如,表单验证失败、权限不足等)。
177
+ - **`BaseRequest` 的行为**: `request` 方法返回的 Promise 将被 **reject**,并将整个 `responseType` 对象作为错误信息传递下去。
178
+
179
+ - **`'login'`**: **需要登录**
180
+ - **含义**: 用户的会话已完全失效,必须重新登录。
181
+ - **`BaseRequest` 的行为**:
182
+ 1. 自动触发您在 `Rule` 中定义的 `login` 函数。
183
+ 2. 在 `login` 函数成功执行后(即其返回的 Promise 被 resolve),`BaseRequest` 会**自动重新发送**之前失败的那个请求。
184
+ 3. 整个过程对最终的调用者是**完全透明的**。
185
+
186
+ - **`'refresh'`**: **需要刷新 Token**
187
+ - **含义**: 用户的访问令牌(Access Token)已过期,但刷新令牌(Refresh Token)仍然有效。
188
+ - **`BaseRequest` 的行为**:
189
+ 1. 自动触发您在 `Rule` 中定义的 `refresh` 函数。
190
+ 2. 在 `refresh` 函数成功执行后,`BaseRequest` 会**自动重新发送**之前失败的那个请求。
191
+ 3. 如果 `refresh` 本身也失败了,或者刷新后重试的请求依然返回 `'refresh'`,`BaseRequest` 会自动将流程降级为 `'login'`。
192
+
193
+ 通过精心设计您的 `parse` 函数,您可以以一种非常优雅和集中的方式,实现极其复杂的认证和重试逻辑。
194
+
195
+ ## 依赖
196
+
197
+ - [complex-utils](https://github.com/MarAngle/complex-utils): 提供核心的工具函数和基类。
198
+ - [complex-plugin](https://github.com/MarAngle/complex-plugin): 提供 `notice` 通知插件。
package/history.md ADDED
@@ -0,0 +1,88 @@
1
+ ### 1.1.2 - 1.1.3
2
+ - refactor: 将TypeScript类型导入语法升级为type关键字形式
3
+
4
+ ### 1.1.1
5
+ - refactor(build): 移除项目的所有构建配置,回归到纯源码模式。
6
+ - feat(test): 集成 `Vitest` 单元测试框架。
7
+ - docs(readme): 创建详细的 `README.md` 文件。
8
+ - chore(history): 全面优化和重构 `history.md` 的格式。
9
+
10
+ ### 1.0.1
11
+ - feat(module): 修改模块加载逻辑为 ES2020。
12
+
13
+ ### 0.6.2
14
+ - refactor(code): 基于 AI 进行全局代码优化。
15
+
16
+ ### 0.6.1
17
+ - chore: 稳定版升级。
18
+
19
+ ### 0.5.10
20
+ - chore(deps): 升级 `complex-plugin` 依赖并优化逻辑。
21
+
22
+ ### 0.5.7
23
+ - refactor(request)!: **[非兼容性更新]** `failNotice` 重命名为 `fail`,`failNotice.local` 重命名为 `fail.intercept`。
24
+ - refactor(request): 优化 `$parseError` 返回的 `type` 类型。
25
+ - refactor(request): 将 `config` 配置项迁移到类的静态数据中。
26
+
27
+ ### 0.5.5
28
+ - refactor(types): 优化泛型类型传递。
29
+
30
+ ### 0.5.4
31
+ - feat(token): 实现 `refreshToken` 的数据持久化保存。
32
+
33
+ ### 0.5.3
34
+ - feat(request): 实现完整的 `login` -> `refresh` -> `retry` 流程。
35
+ - `rule.login` 添加 `trigger` 参数,用于判断登录的触发来源。
36
+ - `responseType.status` 添加 `refresh` 状态,用于触发 `rule.refresh`。
37
+ - 当 `refresh` 失败或再次触发 `refresh` 时,自动降级为 `login`。
38
+
39
+ ### 0.5.1
40
+ - chore(deps): 升级 `complex-plugin` 依赖。
41
+
42
+ ### 0.4.7
43
+ - chore(deps): 升级 `complex-plugin` 依赖。
44
+
45
+ ### 0.4.3
46
+ - refactor(rule)!: **[非兼容性更新]** `BaseRequest` 的 `rule` 简化为单选,以简化逻辑。
47
+
48
+ ### 0.3.6
49
+ - refactor(code): 优化全局的 `undefined` 校验逻辑。
50
+
51
+ ### 0.3.5
52
+ - refactor(rule)!: **[非兼容性更新]** `responseFormat` 重命名为 `responseParse`。
53
+ - refactor(rule)!: **[非兼容性更新]** `Rule` 的 `format` 函数重命名为 `parse`,并新增 `format` 函数用于请求前的格式化。
54
+
55
+ ### 0.3.4
56
+ - chore(deps): 依赖大版本升级。
57
+
58
+ ### 0.2.1
59
+ - refactor(types): 为 `local` 参数实现泛型类型。
60
+
61
+ ### 0.2.0
62
+ - refactor(code): 统一函数命名规则 (外部函数、内部函数、私有函数)。
63
+
64
+ ### 0.1.10
65
+ - chore(deps): 升级依赖,适配 `formatConfig`。
66
+
67
+ ### 0.1.9
68
+ - fix(request): 修正 `requestConfig.data` 被重置为空对象的 BUG。
69
+ - feat(types): 扩展 `BaseRequest` 为泛型类,允许定义最终的响应类型。
70
+
71
+ ### 0.1.8
72
+ - refactor(request)!: **[非兼容性更新]** `Request` 类重命名为 `BaseRequest`。
73
+ - fix(request): 修正 `post/get/form/json` 未正确设置 `method` 的 BUG。
74
+ - chore(deps): 升级依赖。
75
+
76
+ ### 0.1.7
77
+ - fix(deps): 修正错误的依赖项。
78
+
79
+ ### 0.1.6
80
+ - refactor(request): 优化请求参数和失败错误处理。
81
+ - feat(request): 扩展失败信息提示。
82
+
83
+ ### 0.1.2
84
+ - feat(request): 扩展支持的 HTTP 状态码错误信息。
85
+ - refactor(code): 优化函数和类型定义。
86
+
87
+ ### 0.1.1
88
+ - feat: 项目初始化,实现基础的请求功能。
package/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ import BaseRequest, { type RequestConfig, type RequestInitOption, type requestTrigger } from './src/BaseRequest'
2
+ import Rule from './src/Rule'
3
+ import Token from './src/Token'
4
+
5
+ export {
6
+ BaseRequest,
7
+ type RequestConfig,
8
+ type RequestInitOption,
9
+ type requestTrigger,
10
+ Rule,
11
+ Token
12
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@complex-suite/request",
3
+ "version": "1.1.3",
4
+ "description": "a complex request",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/MarAngle/complex-request.git"
10
+ },
11
+ "dependencies": {
12
+ "@complex-suite/utils": "2.10.5",
13
+ "@complex-suite/plugin": "4.10.2"
14
+ },
15
+ "devDependencies": {
16
+ "jsdom": "^27.1.0",
17
+ "typescript": "^5.2.2",
18
+ "vitest": "^4.0.7"
19
+ },
20
+ "keywords": [
21
+ "complex",
22
+ "ajax",
23
+ "fetch",
24
+ "require",
25
+ "request"
26
+ ],
27
+ "author": "MarAngle",
28
+ "license": "ISC",
29
+ "scripts": {
30
+ "check": "tsc --noEmit",
31
+ "test": "vitest",
32
+ "coverage": "vitest run --coverage",
33
+ "release": "npm publish --registry=https://registry.npmjs.org"
34
+ }
35
+ }
@@ -0,0 +1,333 @@
1
+ import { _Data, jsonToForm } from "@complex-suite/utils"
2
+ import { notice } from "@complex-suite/plugin"
3
+ import type { messageType } from "@complex-suite/plugin"
4
+ import Rule from "./Rule"
5
+ import type { RuleInitOption, responseType } from "./Rule"
6
+
7
+ type statusType = {
8
+ [prop: number]: string
9
+ }
10
+
11
+ export type formatUrlType = (url: string) => string
12
+
13
+ export interface RequestInitOption<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> {
14
+ baseUrl?: string
15
+ status?: statusType
16
+ formatUrl?: formatUrlType
17
+ rule: RuleInitOption<R, L>
18
+ }
19
+
20
+ export type methodType = 'get' | 'post' | 'delete' | 'put' | 'patch' | 'head' | 'options'
21
+
22
+ export type failType = 'internal' | 'server'
23
+
24
+ export type totalFailType = 'token' | failType
25
+
26
+ export type failOption = {
27
+ intercept?: totalFailType[] // 内部报错判断值
28
+ content?: string
29
+ duration?: number
30
+ type?: messageType
31
+ title?: string
32
+ }
33
+
34
+ const defaultFormatUrlWithBaseUrl = function(this: BaseRequest, url: string) {
35
+ if (!url.startsWith('https://') && !url.startsWith('http://')) {
36
+ // 当前URL不以http/https开始,则认为此URL需要添加默认前缀
37
+ url = this.baseUrl + url
38
+ }
39
+ return url
40
+ }
41
+
42
+ const defaultFormatUrl = function(url: string) {
43
+ return url
44
+ }
45
+
46
+ export type requestTrigger = 'login' | 'refresh'
47
+
48
+ export interface RequestConfig<_R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> {
49
+ url: string // 请求地址
50
+ method: methodType // 请求方式
51
+ headers: Record<string, undefined | null | string | number | boolean> // Header头
52
+ data: Record<PropertyKey, unknown> | FormData // Body体
53
+ params: Record<PropertyKey, unknown> // query数据
54
+ token: boolean | string[] // Token
55
+ format?: (finalConfig: L, trigger?: requestTrigger) => unknown // 对最终的数据做格式化处理,此数据为对应请求插件的参数而非Request的参数
56
+ currentType: 'json' | 'form' // 当前数据类型
57
+ targetType?: 'json' | 'form' // 目标数据类型=>初始化参数,后期无效
58
+ responseType: 'json' | 'text' | 'blob' // 返回值类型,仅json进行格式化
59
+ responseParse: boolean // 返回值解析判断,为否不解析
60
+ fail: false | failOption
61
+ local?: L // 请求插件的单独参数
62
+ }
63
+
64
+ abstract class BaseRequest<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> extends _Data {
65
+ static $name = 'BaseRequest'
66
+ static $formatConfig = { name: 'Request:BaseRequest', level: 5, recommend: false }
67
+ static $status = {
68
+ 400: '错误请求!',
69
+ 403: '拒绝访问!',
70
+ 404: '很抱歉,资源未找到!',
71
+ 405: '请求方法不支持!',
72
+ 408: '请求超时!',
73
+ 410: '请求资源已删除!',
74
+ 500: '服务器内部错误!',
75
+ 502: '错误网关!',
76
+ 503: '服务不可用!',
77
+ 504: '网关超时!',
78
+ 505: 'HTTP版本不受支持!'
79
+ }
80
+ static $contentType = {
81
+ json: undefined,
82
+ form: 'multipart/form-data'
83
+ }
84
+ static $fail = {
85
+ message: {
86
+ internal: '请求终止,请求发送失败!',
87
+ server: '服务器请求失败,请刷新重试或联系管理员!'
88
+ } as Record<totalFailType, undefined | string>,
89
+ option: {
90
+ token: {
91
+ type: 'error',
92
+ title: 'Token错误'
93
+ },
94
+ internal: {
95
+ type: 'error',
96
+ title: '请求错误'
97
+ },
98
+ server: {
99
+ type: 'error',
100
+ title: '请求失败'
101
+ },
102
+ } as Record<totalFailType, failOption>
103
+ }
104
+ baseUrl?: string
105
+ isLogining?: Promise<any>
106
+ isRefreshing?: Promise<any>
107
+ status: statusType
108
+ formatUrl: formatUrlType
109
+ rule: Rule<R, L>
110
+ constructor(initOption: RequestInitOption<R, L>) {
111
+ super()
112
+ this.baseUrl = initOption.baseUrl
113
+ this.status = {
114
+ ...(this.constructor as typeof BaseRequest).$status,
115
+ ...initOption.status
116
+ }
117
+ this.formatUrl = this._getFormatUrl(initOption.formatUrl)
118
+ this.rule = new Rule(initOption.rule)
119
+ }
120
+ protected _getFormatUrl(formatUrl?: formatUrlType) {
121
+ if (formatUrl) {
122
+ return formatUrl
123
+ } else if (this.baseUrl) {
124
+ return defaultFormatUrlWithBaseUrl
125
+ } else {
126
+ return defaultFormatUrl
127
+ }
128
+ }
129
+ protected _syncFormatUrl() {
130
+ // 当前格式化URL函数为默认函数时则进行重新获取操作
131
+ if (this.formatUrl === defaultFormatUrlWithBaseUrl || this.formatUrl === defaultFormatUrl) {
132
+ this.formatUrl = this._getFormatUrl()
133
+ }
134
+ }
135
+ changeBaseUrl(baseUrl: string) {
136
+ this.baseUrl = baseUrl || ''
137
+ this._syncFormatUrl()
138
+ }
139
+ protected _parseRequestConfig(requestConfig: Partial<RequestConfig<R, L>>): RequestConfig<R, L> {
140
+ requestConfig.url = this.formatUrl(requestConfig.url!)
141
+ if (!requestConfig.method) {
142
+ requestConfig.method = 'get'
143
+ }
144
+ const targetType = requestConfig.targetType || 'json'
145
+ if (requestConfig.currentType == undefined) {
146
+ requestConfig.currentType = 'json'
147
+ }
148
+ const $contentType = (this.constructor as typeof BaseRequest).$contentType
149
+ const defaultContentType = targetType === 'json' ? $contentType.json : $contentType.form
150
+ if (!requestConfig.headers) {
151
+ requestConfig.headers = defaultContentType != undefined ? {
152
+ 'Content-Type': defaultContentType
153
+ } : {}
154
+ } else if (requestConfig.headers['Content-Type'] == undefined && defaultContentType != undefined) {
155
+ requestConfig.headers['Content-Type'] = defaultContentType
156
+ }
157
+ if (!requestConfig.data) {
158
+ requestConfig.data = targetType === 'form' ? new FormData() : {}
159
+ } else if (requestConfig.currentType !== targetType) {
160
+ if (requestConfig.currentType === 'json') {
161
+ requestConfig.data = jsonToForm(requestConfig.data)
162
+ } else {
163
+ const data: Record<PropertyKey, unknown> = {};
164
+ (requestConfig.data as FormData).forEach((value, key) => {
165
+ (data as Record<PropertyKey, unknown>)[key] = value
166
+ })
167
+ requestConfig.data = data
168
+ }
169
+ }
170
+ requestConfig.currentType = targetType
171
+ if (requestConfig.token == undefined) {
172
+ requestConfig.token = true
173
+ }
174
+ if (!requestConfig.params) {
175
+ requestConfig.params = {}
176
+ }
177
+ if (requestConfig.responseType == undefined) {
178
+ requestConfig.responseType = 'json'
179
+ }
180
+ if (requestConfig.responseParse == undefined) {
181
+ requestConfig.responseParse = true
182
+ }
183
+ if (requestConfig.fail == undefined) {
184
+ requestConfig.fail = {}
185
+ }
186
+ return requestConfig as RequestConfig<R, L>
187
+ }
188
+ request(requestConfig: Partial<RequestConfig<R, L>>) {
189
+ return this._request(this._parseRequestConfig(requestConfig))
190
+ }
191
+ protected _showFail(fail: false | failOption, from: totalFailType, msg?: string) {
192
+ if (fail !== false) {
193
+ if (fail.intercept && fail.intercept.indexOf(from) > -1) {
194
+ // 存在拦截时判断类型在拦截范围内直接返回不输出错误信息
195
+ return
196
+ }
197
+ const content = fail.content || msg
198
+ if (content) {
199
+ const $failOption = (this.constructor as typeof BaseRequest).$fail.option[from]
200
+ notice.message(content, fail.type || $failOption.type, fail.title || $failOption.title, fail.duration || $failOption.duration)
201
+ }
202
+ }
203
+ }
204
+ protected _request(requestConfig: RequestConfig<R, L>, trigger?: requestTrigger): Promise<responseType> {
205
+ const res = this.rule.$appendToken(requestConfig)
206
+ if (res) {
207
+ if (res.token) {
208
+ // 存在Token规则但是不存在值,需要调用login接口
209
+ // 此处不应判断是否为重复操作
210
+ return this._handleTokenLogin(requestConfig)
211
+ } else {
212
+ const msg = `${res.prop}对应的Token规则不存在!`
213
+ this.$exportMsg(msg)
214
+ this._showFail(requestConfig.fail, 'token', msg)
215
+ return Promise.reject({ status: 'fail', code: 'token absent' })
216
+ }
217
+ }
218
+ return this._handleRequest(requestConfig, trigger)
219
+ }
220
+ private _handleTokenLogin(requestConfig: RequestConfig<R, L>): Promise<responseType> {
221
+ return new Promise((resolve, reject) => {
222
+ this.rule.login('token').then(() => {
223
+ this._request(requestConfig, 'login').then(resolve).catch(reject)
224
+ }).catch(reject)
225
+ })
226
+ }
227
+ private _handleRequest(requestConfig: RequestConfig<R, L>, trigger?: requestTrigger): Promise<responseType> {
228
+ return new Promise((resolve, reject) => {
229
+ this.$request(requestConfig, trigger).then(response => {
230
+ if (requestConfig.responseParse && requestConfig.responseType === 'json') {
231
+ const finalResponse = this.rule.parse(response, requestConfig)
232
+ if (finalResponse.status === 'success') {
233
+ resolve(finalResponse)
234
+ } else if (finalResponse.status === 'refresh') {
235
+ if (this.rule.refresh && trigger !== 'refresh') {
236
+ // 当前请求提示login说明请求前的token验证通过
237
+ // 此时在第一次需要登陆时进行this.rule.refresh的操作,进行可能的刷新Token机制
238
+ // 此刷新机制失败则触发登录
239
+ // 如登录后依然需要登录则按照失败处理(此处的登录可能是由本地token验证失败触发的登录)
240
+ if (!this.isRefreshing) {
241
+ this.isRefreshing = this.rule.refresh()
242
+ }
243
+ this.isRefreshing.then(() => {
244
+ this._request(requestConfig, 'refresh').then(resolve).catch(reject).finally(() => {
245
+ this.isRefreshing = undefined
246
+ })
247
+ }).catch(reject)
248
+ } else if (trigger !== 'login') {
249
+ if (!this.isLogining) {
250
+ this.isLogining = this.rule.login('refresh')
251
+ }
252
+ this.isLogining.then(() => {
253
+ this._request(requestConfig, 'login').then(resolve).catch(reject).finally(() => {
254
+ this.isLogining = undefined
255
+ })
256
+ }).catch(reject)
257
+ } else {
258
+ reject(finalResponse)
259
+ }
260
+ } else if (finalResponse.status === 'login') {
261
+ if (!this.isLogining) {
262
+ this.isLogining = this.rule.login('login')
263
+ }
264
+ this.isLogining.then(() => {
265
+ this._request(requestConfig, 'login').then(resolve).catch(reject).finally(() => {
266
+ this.isLogining = undefined
267
+ })
268
+ }).catch(reject)
269
+ } else if (finalResponse.status === 'fail') {
270
+ this._showFail(requestConfig.fail, 'server', finalResponse.msg)
271
+ reject(finalResponse)
272
+ }
273
+ } else {
274
+ resolve({ status: 'success', code: 'origin', data: response })
275
+ }
276
+ }).catch(error => {
277
+ const err = this.$parseError(error)
278
+ const $failOption = (this.constructor as typeof BaseRequest).$fail
279
+ this._showFail(requestConfig.fail, err.type, err.msg || $failOption.message[err.type])
280
+ reject({ status: 'fail', code: err.type + ' error', err: error })
281
+ })
282
+ })
283
+ }
284
+ // 重要: requestConfig需要深拷贝到具体实例中而非直接引用,此处保证在login/refresh时的requestConfig保持一致
285
+ abstract $request(requestConfig: RequestConfig<R, L>, from?: requestTrigger): Promise<R>
286
+ abstract $parseError(responseError: unknown): { msg?: string, type: failType, data: unknown }
287
+ get(requestConfig: Partial<RequestConfig<R, L>>) {
288
+ requestConfig.method = 'get'
289
+ return this.request(requestConfig)
290
+ }
291
+ post(requestConfig: Partial<RequestConfig<R, L>>) {
292
+ requestConfig.method = 'post'
293
+ return this.request(requestConfig)
294
+ }
295
+ form(requestConfig: Partial<RequestConfig<R, L>>) {
296
+ requestConfig.method = 'post'
297
+ requestConfig.currentType = 'form'
298
+ requestConfig.targetType = 'form'
299
+ return this.request(requestConfig)
300
+ }
301
+ json(requestConfig: Partial<RequestConfig<R, L>>) {
302
+ requestConfig.method = 'post'
303
+ requestConfig.currentType = 'json'
304
+ requestConfig.targetType = 'form'
305
+ return this.request(requestConfig)
306
+ }
307
+ setToken(tokenName: string, value: unknown, unSave?: boolean) {
308
+ this.rule.setToken(tokenName, value, unSave)
309
+ }
310
+ getToken(tokenName: string) {
311
+ return this.rule.getToken(tokenName)
312
+ }
313
+ clearToken(tokenName: string | true) {
314
+ return this.rule.clearToken(tokenName)
315
+ }
316
+ destroyToken(tokenName: string | true) {
317
+ return this.rule.destroyToken(tokenName)
318
+ }
319
+ setRefreshToken(value: unknown, unSave?: boolean) {
320
+ this.rule.setRefreshToken(value, unSave)
321
+ }
322
+ getRefreshToken() {
323
+ return this.rule.getRefreshToken()
324
+ }
325
+ clearRefreshToken() {
326
+ return this.rule.clearRefreshToken()
327
+ }
328
+ destroyRefreshToken() {
329
+ return this.rule.destroyRefreshToken()
330
+ }
331
+ }
332
+
333
+ export default BaseRequest
package/src/Rule.ts ADDED
@@ -0,0 +1,186 @@
1
+ import { _Data } from "@complex-suite/utils"
2
+ import Token from "./Token"
3
+ import type { TokenInitOption } from "./Token"
4
+ import type { RequestConfig } from "./BaseRequest"
5
+
6
+ export type tokenType = {
7
+ time?: number
8
+ session?: boolean
9
+ data?: Record<string, TokenInitOption>
10
+ refreshToken?: TokenInitOption
11
+ }
12
+
13
+ export interface responseType<D = any> {
14
+ status: 'success' | 'fail' | 'login' | 'refresh'
15
+ data: D
16
+ code?: number | string
17
+ msg?: string
18
+ err?: string | Error | Record<PropertyKey, unknown>
19
+ }
20
+
21
+ type formatType<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> = (requestConfig: RequestConfig<R, L>) => void
22
+ type parseType<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> = (response: R, requestConfig: RequestConfig<R, L>) => responseType
23
+ type formatUrlType = (url: string) => string
24
+ type loginType = (trigger: 'token' | 'refresh' | 'login') => Promise<unknown>
25
+ type refreshType = () => Promise<unknown>
26
+
27
+ export interface RuleInitOption<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> {
28
+ prop: string
29
+ token?: tokenType
30
+ format?: formatType<R, L> // 跟登录无关的参数在这里进行赋值,避免token过多导致的token失效后的连锁反应,注意此时的requestConfig已经经过了token的判断,data可能为formdata
31
+ parse: parseType<R, L> // 格式化返回参数
32
+ login: loginType // 登录操作,触发于token本地验证失败时\接口login\接口refresh成功后重新调用依然需要refresh时
33
+ refresh: refreshType // 刷新操作,触发于请求提示refresh时
34
+ formatUrl?: formatUrlType // 格式化对应URL
35
+ }
36
+
37
+ function defaultFormatUrl(url: string) {
38
+ return url
39
+ }
40
+
41
+ class Rule<R = Record<PropertyKey, unknown>, L = Record<PropertyKey, unknown>> extends _Data{
42
+ static $name = 'Rule'
43
+ static $formatConfig = { name: 'Request:Rule', level: 5, recommend: false }
44
+ prop: string
45
+ token: Record<string, Token>
46
+ refreshToken?: Token
47
+ format?: formatType<R, L>
48
+ parse: parseType<R, L>
49
+ login: loginType
50
+ refresh: refreshType
51
+ formatUrl: formatUrlType
52
+ constructor(initOption: RuleInitOption<R, L>) {
53
+ super()
54
+ this.prop = initOption.prop
55
+ this.token = {}
56
+ if (initOption.token) {
57
+ if (initOption.token.data) {
58
+ for (const tokenName in initOption.token.data) {
59
+ this.token[tokenName] = new Token(initOption.token.data[tokenName], tokenName, this.prop, initOption.token.time, initOption.token.session)
60
+ }
61
+ }
62
+ if (initOption.token.refreshToken) {
63
+ this.refreshToken = new Token(initOption.token.refreshToken, 'refreshToken', this.prop, initOption.token.time, initOption.token.session)
64
+ }
65
+ }
66
+ this.format = initOption.format
67
+ this.parse = initOption.parse
68
+ this.login = initOption.login
69
+ this.refresh = initOption.refresh
70
+ this.formatUrl = initOption.formatUrl || defaultFormatUrl
71
+ }
72
+ $appendToken(requestConfig: RequestConfig<R, L>) {
73
+ const tokenList = requestConfig.token === true ? Object.keys(this.token) : requestConfig.token
74
+ if (tokenList) {
75
+ for (const tokenName of tokenList) {
76
+ const token = this.token[tokenName]
77
+ if (token) {
78
+ if (!token.$appendValue(requestConfig, tokenName)) {
79
+ return { prop: tokenName, token: true, value: false }
80
+ }
81
+ } else {
82
+ return { prop: tokenName, token: false }
83
+ }
84
+ }
85
+ }
86
+ if (this.format) {
87
+ this.format(requestConfig)
88
+ }
89
+ return
90
+ }
91
+ setToken(tokenName: string, value: unknown, unSave?: boolean) {
92
+ if (this.token[tokenName]) {
93
+ this.token[tokenName].setValue(value, unSave)
94
+ } else {
95
+ this.$exportMsg(`未找到${tokenName}对应的Token规则,setToken失败!`, 'error')
96
+ }
97
+ }
98
+ getToken (tokenName: string) {
99
+ if (this.token[tokenName]) {
100
+ return this.token[tokenName].getValue()
101
+ } else {
102
+ this.$exportMsg(`未找到${tokenName}对应的Token规则,getToken失败!`, 'error')
103
+ }
104
+ }
105
+ setRefreshToken(value: unknown, unSave?: boolean) {
106
+ if (this.refreshToken) {
107
+ this.refreshToken.setValue(value, unSave)
108
+ } else {
109
+ this.$exportMsg(`未找到refreshToken对应的Token规则,setRefreshToken失败!`, 'error')
110
+ }
111
+ }
112
+ getRefreshToken() {
113
+ if (this.refreshToken) {
114
+ return this.refreshToken.getValue()
115
+ } else {
116
+ this.$exportMsg(`未找到refreshToken对应的Token规则,getRefreshToken失败!`, 'error')
117
+ }
118
+ }
119
+ clearRefreshToken() {
120
+ if (this.refreshToken) {
121
+ this.refreshToken.clear()
122
+ }
123
+ }
124
+ destroyRefreshToken() {
125
+ if (this.refreshToken) {
126
+ this.refreshToken.destroy()
127
+ delete this.refreshToken
128
+ }
129
+ }
130
+ protected _clearToken(tokenName: string) {
131
+ if (this.token[tokenName]) {
132
+ this.token[tokenName].clear()
133
+ return true
134
+ } else {
135
+ this.$exportMsg(`未找到${tokenName}对应的token规则,clearToken失败!`, 'warn')
136
+ return false
137
+ }
138
+ }
139
+ protected _destroyToken(tokenName: string) {
140
+ if (this.token[tokenName]) {
141
+ this.token[tokenName].destroy()
142
+ delete this.token[tokenName]
143
+ return true
144
+ } else {
145
+ this.$exportMsg(`未找到${tokenName}对应的token规则,destroyToken失败!`, 'warn')
146
+ return false
147
+ }
148
+ }
149
+ clearToken(tokenName: true | string) {
150
+ if (tokenName) {
151
+ if (tokenName === true) {
152
+ for (const n in this.token) {
153
+ this._clearToken(n)
154
+ }
155
+ this.clearRefreshToken()
156
+ return true
157
+ } else {
158
+ return this._clearToken(tokenName)
159
+ }
160
+ } else {
161
+ this.$exportMsg(`未指定需要清除的token!`)
162
+ return false
163
+ }
164
+ }
165
+ destroyToken(tokenName: true | string) {
166
+ if (tokenName) {
167
+ if (tokenName === true) {
168
+ for (const n in this.token) {
169
+ this._destroyToken(n)
170
+ }
171
+ this.destroyRefreshToken()
172
+ return true
173
+ } else {
174
+ return this._destroyToken(tokenName)
175
+ }
176
+ } else {
177
+ this.$exportMsg(`未指定需要销毁的token!`)
178
+ return false
179
+ }
180
+ }
181
+ _getName() {
182
+ return `${this._getConstructorName()}:${this.prop}`
183
+ }
184
+ }
185
+
186
+ export default Rule
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import Token from './Token';
3
+
4
+ describe('Token', () => {
5
+ it('should be instantiated correctly with default values', () => {
6
+ const token = new Token({}, 'testToken', 'testRule');
7
+ expect(token).toBeInstanceOf(Token);
8
+ expect(token.location).toBe('body');
9
+ expect(token.require).toBeUndefined();
10
+ });
11
+
12
+ it('should store and retrieve a value from memory', () => {
13
+ const token = new Token({ value: 'my-secret-token' }, 'testToken', 'testRule');
14
+ expect(token.getValue()).toBe('my-secret-token');
15
+ });
16
+
17
+ it('should respect the require flag', () => {
18
+ const requiredToken = new Token({ require: true }, 'requiredToken', 'testRule');
19
+ const optionalToken = new Token({ require: false }, 'optionalToken', 'testRule');
20
+
21
+ expect(requiredToken.$checkValue(undefined)).toBe(false);
22
+ expect(requiredToken.$checkValue(null)).toBe(false);
23
+ expect(requiredToken.$checkValue('some-value')).toBe(true);
24
+
25
+ expect(optionalToken.$checkValue(undefined)).toBe(true);
26
+ });
27
+
28
+ it('should append value to the correct location', () => {
29
+ const headerToken = new Token({ value: 'header-val', location: 'header' }, 'hToken', 'testRule');
30
+ const bodyToken = new Token({ value: 'body-val', location: 'body' }, 'bToken', 'testRule');
31
+ const paramsToken = new Token({ value: 'params-val', location: 'params' }, 'pToken', 'testRule');
32
+
33
+ const config = {
34
+ headers: {},
35
+ data: {},
36
+ params: {}
37
+ };
38
+
39
+ headerToken.$appendValue(config as any, 'Authorization');
40
+ bodyToken.$appendValue(config as any, 'token');
41
+ paramsToken.$appendValue(config as any, 'api_key');
42
+
43
+ expect(config.headers).toEqual({ 'Authorization': 'header-val' });
44
+ expect(config.data).toEqual({ 'token': 'body-val' });
45
+ expect(config.params).toEqual({ 'api_key': 'params-val' });
46
+ });
47
+ });
package/src/Token.ts ADDED
@@ -0,0 +1,174 @@
1
+ import { isExist, storage, appendProp } from '@complex-suite/utils'
2
+ import type { RequestConfig } from './BaseRequest'
3
+
4
+ type getValueType = () => unknown
5
+ type removeValueType = getValueType
6
+ type clearType = getValueType
7
+ type isExistType = (data: unknown) => boolean
8
+ type destroyType = clearType
9
+
10
+ export type locationType = 'body' | 'header' | 'params'
11
+
12
+ export interface TokenInitOption {
13
+ value?: unknown
14
+ require?: boolean // 是否必选,必选则会在isExist返回不存在时失败,可能触发rule.login
15
+ location?: locationType // 位置
16
+ time?: number // 本地缓存有效期
17
+ session?: boolean // 本地缓存是否为session
18
+ getValue?: getValueType // 获取value函数实现,如存在此函数则不会直接从value中取值
19
+ isExist?: isExistType // 判断数据是否存在,用户require判断和缓存获取判断
20
+ clear?: clearType // 清除数据
21
+ destroy?: destroyType // 销毁数据/会先触发清除数据
22
+ }
23
+
24
+ function setValue(this: Token, data: unknown, unSave?: boolean) {
25
+ this.value = data
26
+ if (!unSave) {
27
+ storage.setData(this.prop, data)
28
+ }
29
+ }
30
+
31
+ function setValueBySession(this: Token, data: unknown, unSave?: boolean) {
32
+ this.value = data
33
+ if (!unSave) {
34
+ storage.setSessionData(this.prop, data)
35
+ }
36
+ }
37
+
38
+ function getValue(this: Token) {
39
+ let data = this.$getValue!()
40
+ if (!this.isExist(data)) {
41
+ data = storage.getData(this.prop, this.time)
42
+ if (this.isExist(data)) {
43
+ this.setValue(data, true)
44
+ }
45
+ }
46
+ return data
47
+ }
48
+
49
+ function getValueBySession(this: Token) {
50
+ let data = this.$getValue!()
51
+ if (!this.isExist(data)) {
52
+ data = storage.getSessionData(this.prop, this.time)
53
+ if (this.isExist(data)) {
54
+ this.setValue(data, true)
55
+ }
56
+ }
57
+ return data
58
+ }
59
+
60
+ function getValueFromValue(this: Token) {
61
+ let data = this.value
62
+ if (!this.isExist(data)) {
63
+ data = storage.getData(this.prop, this.time)
64
+ if (this.isExist(data)) {
65
+ this.setValue(data, true)
66
+ }
67
+ }
68
+ return data
69
+ }
70
+
71
+ function getValueFromValueBySession(this: Token) {
72
+ let data = this.value
73
+ if (!this.isExist(data)) {
74
+ data = storage.getSessionData(this.prop, this.time)
75
+ if (this.isExist(data)) {
76
+ this.setValue(data, true)
77
+ }
78
+ }
79
+ return data
80
+ }
81
+
82
+ function removeValue(this: Token) {
83
+ storage.removeData(this.prop)
84
+ this.value = undefined
85
+ }
86
+
87
+ function removeValueBySession(this: Token) {
88
+ storage.removeSessionData(this.prop)
89
+ this.value = undefined
90
+ }
91
+
92
+ class Token {
93
+ static $name = 'Token'
94
+ prop: string
95
+ require?: boolean
96
+ value?: unknown
97
+ location: locationType
98
+ time?: number
99
+ isExist: isExistType
100
+ setValue: (data: unknown, unSave?: boolean) => void
101
+ $getValue?: getValueType
102
+ getValue: getValueType
103
+ removeValue: removeValueType
104
+ $destroy?: destroyType
105
+ $clear?: clearType
106
+ constructor(initOption: TokenInitOption, prop: string, ruleProp: string, time?: number, session?: boolean) {
107
+ this.prop = `require-${prop}-${ruleProp}`
108
+ if (initOption.require !== undefined) {
109
+ this.require = initOption.require
110
+ }
111
+ if (initOption.value !== undefined) {
112
+ this.value = initOption.value
113
+ }
114
+ this.location = initOption.location || 'body'
115
+ if (initOption.time === undefined) {
116
+ if (time !== undefined) {
117
+ this.time = time
118
+ }
119
+ } else {
120
+ this.time = initOption.time
121
+ }
122
+ if (initOption.session != undefined) {
123
+ session = initOption.session
124
+ }
125
+ this.isExist = initOption.isExist || isExist
126
+ this.setValue = !session ? setValue : setValueBySession
127
+ if (initOption.getValue) {
128
+ this.$getValue = initOption.getValue
129
+ this.getValue = !session ? getValue : getValueBySession
130
+ } else {
131
+ this.getValue = !session ? getValueFromValue : getValueFromValueBySession
132
+ }
133
+ this.removeValue = !session ? removeValue : removeValueBySession
134
+ this.$clear = initOption.clear
135
+ this.$destroy = initOption.destroy
136
+ }
137
+ $appendValue(requestConfig: RequestConfig<any, any>, tokenName: string) {
138
+ const value = this.getValue()
139
+ if (this.$checkValue(value)) {
140
+ const location = this.location
141
+ if (location === 'body') {
142
+ appendProp(requestConfig.data as Record<PropertyKey, unknown>, tokenName, value, requestConfig.currentType as 'json')
143
+ } else if (location === 'header') {
144
+ requestConfig.headers[tokenName] = value as string
145
+ } else if (location === 'params') {
146
+ requestConfig.params[tokenName] = value
147
+ }
148
+ return true
149
+ } else {
150
+ return false
151
+ }
152
+ }
153
+ $checkValue(data: unknown) {
154
+ if (this.require && !this.isExist(data)) {
155
+ // 数据必选且不存在时返回失败
156
+ return false
157
+ }
158
+ return true
159
+ }
160
+ clear() {
161
+ this.removeValue()
162
+ if (this.$clear) {
163
+ this.$clear()
164
+ }
165
+ }
166
+ destroy() {
167
+ this.clear()
168
+ if (this.$destroy) {
169
+ this.$destroy()
170
+ }
171
+ }
172
+ }
173
+
174
+ export default Token
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"]
4
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'jsdom',
7
+ include: ['src/**/*.test.ts'],
8
+ },
9
+ })