axios-annotations 2.3.0 → 3.1.0
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 +774 -741
- package/es/core/builder.d.ts +33 -0
- package/es/core/builder.js +515 -0
- package/es/core/common.d.ts +36 -0
- package/es/core/common.js +148 -0
- package/es/core/config.d.ts +60 -0
- package/es/core/config.js +357 -0
- package/es/core/core-expect.js +3 -0
- package/es/core/expect.d.ts +2 -0
- package/es/core/expect.js +2 -0
- package/es/core/provider.d.ts +6 -0
- package/es/core/provider.js +172 -0
- package/es/core/service.d.ts +5 -0
- package/es/core/service.js +57 -0
- package/es/decorator/path-variables.d.ts +7 -0
- package/es/decorator/path-variables.js +32 -0
- package/es/decorator/request-body.d.ts +7 -0
- package/es/decorator/request-body.js +31 -0
- package/es/decorator/request-config.d.ts +7 -0
- package/es/decorator/request-config.js +25 -0
- package/es/decorator/request-header.d.ts +4 -0
- package/es/decorator/request-header.js +14 -0
- package/es/decorator/request-mapping.d.ts +6 -0
- package/es/decorator/request-mapping.js +56 -0
- package/es/decorator/request-param.d.ts +4 -0
- package/es/decorator/request-param.js +15 -0
- package/es/decorator/request-with.d.ts +3 -0
- package/es/decorator/request-with.js +8 -0
- package/es/index.d.ts +13 -0
- package/es/index.js +14 -0
- package/es/plugins/auth/authorizer.d.ts +24 -0
- package/es/plugins/auth/authorizer.js +268 -0
- package/es/plugins/auth/history.d.ts +15 -0
- package/es/plugins/auth/history.js +94 -0
- package/es/plugins/auth/index.d.ts +16 -0
- package/es/plugins/auth/index.js +358 -0
- package/es/plugins/auth/queue.d.ts +29 -0
- package/es/plugins/auth/queue.js +281 -0
- package/es/plugins/auth/storage.d.ts +6 -0
- package/es/plugins/auth/storage.js +203 -0
- package/lib/core/builder.d.ts +33 -0
- package/lib/core/builder.js +520 -0
- package/lib/core/common.d.ts +34 -4
- package/lib/core/common.js +139 -147
- package/lib/core/config.d.ts +15 -15
- package/lib/core/config.js +44 -14
- package/lib/core/core-expect.js +8 -0
- package/lib/core/expect.d.ts +2 -4
- package/lib/core/expect.js +6 -15
- package/lib/core/provider.d.ts +5 -5
- package/lib/core/provider.js +2 -2
- package/lib/core/service.d.ts +3 -151
- package/lib/core/service.js +43 -544
- package/lib/decorator/path-variables.d.ts +7 -0
- package/lib/decorator/path-variables.js +37 -0
- package/lib/decorator/request-body.d.ts +7 -1
- package/lib/decorator/request-body.js +28 -20
- package/lib/decorator/request-config.d.ts +6 -4
- package/lib/decorator/request-config.js +23 -16
- package/lib/decorator/request-header.d.ts +4 -2
- package/lib/decorator/request-header.js +11 -13
- package/lib/decorator/request-mapping.d.ts +6 -3
- package/lib/decorator/request-mapping.js +46 -43
- package/lib/decorator/request-param.d.ts +4 -1
- package/lib/decorator/request-param.js +12 -19
- package/lib/decorator/request-with.d.ts +3 -1
- package/lib/decorator/request-with.js +6 -13
- package/lib/index.d.ts +4 -11
- package/lib/index.js +19 -68
- package/lib/plugins/auth/authorizer.d.ts +6 -5
- package/lib/plugins/auth/authorizer.js +3 -2
- package/lib/plugins/auth/history.d.ts +5 -4
- package/lib/plugins/auth/history.js +2 -0
- package/lib/plugins/auth/index.d.ts +7 -0
- package/lib/plugins/auth/index.js +11 -3
- package/lib/plugins/auth/queue.d.ts +3 -2
- package/lib/plugins/auth/queue.js +31 -24
- package/lib/plugins/auth/storage.js +2 -2
- package/package.json +42 -7
- package/wechat-mp/core/builder.d.ts +33 -0
- package/wechat-mp/core/builder.js +520 -0
- package/wechat-mp/core/common.d.ts +36 -0
- package/wechat-mp/core/common.js +158 -0
- package/wechat-mp/core/config.d.ts +60 -0
- package/wechat-mp/core/config.js +362 -0
- package/wechat-mp/core/core-expect.js +8 -0
- package/wechat-mp/core/expect.d.ts +2 -0
- package/wechat-mp/core/expect.js +6 -0
- package/wechat-mp/core/provider.d.ts +6 -0
- package/wechat-mp/core/provider.js +178 -0
- package/wechat-mp/core/service.d.ts +5 -0
- package/wechat-mp/core/service.js +61 -0
- package/wechat-mp/decorator/path-variables.d.ts +7 -0
- package/wechat-mp/decorator/path-variables.js +37 -0
- package/wechat-mp/decorator/request-body.d.ts +7 -0
- package/wechat-mp/decorator/request-body.js +36 -0
- package/wechat-mp/decorator/request-config.d.ts +7 -0
- package/wechat-mp/decorator/request-config.js +30 -0
- package/wechat-mp/decorator/request-header.d.ts +4 -0
- package/wechat-mp/decorator/request-header.js +19 -0
- package/wechat-mp/decorator/request-mapping.d.ts +6 -0
- package/wechat-mp/decorator/request-mapping.js +61 -0
- package/wechat-mp/decorator/request-param.d.ts +4 -0
- package/wechat-mp/decorator/request-param.js +20 -0
- package/wechat-mp/decorator/request-with.d.ts +3 -0
- package/wechat-mp/decorator/request-with.js +13 -0
- package/wechat-mp/index.d.ts +13 -0
- package/wechat-mp/index.js +97 -0
- package/wechat-mp/plugins/auth/authorizer.d.ts +24 -0
- package/wechat-mp/plugins/auth/authorizer.js +272 -0
- package/wechat-mp/plugins/auth/history.d.ts +15 -0
- package/wechat-mp/plugins/auth/history.js +98 -0
- package/wechat-mp/plugins/auth/index.d.ts +16 -0
- package/wechat-mp/plugins/auth/index.js +376 -0
- package/wechat-mp/plugins/auth/queue.d.ts +29 -0
- package/wechat-mp/plugins/auth/queue.js +285 -0
- package/wechat-mp/plugins/auth/storage.d.ts +6 -0
- package/wechat-mp/plugins/auth/storage.js +207 -0
- package/index.d.ts +0 -1
- package/index.js +0 -127
- package/lib/core/cancel.d.ts +0 -30
- package/lib/core/cancel.js +0 -56
- package/lib/core/parser.d.ts +0 -19
- package/lib/core/parser.js +0 -79
- package/lib/decorator/abort-source.d.ts +0 -3
- package/lib/decorator/abort-source.js +0 -25
- package/lib/decorator/delete-mapping.d.ts +0 -1
- package/lib/decorator/delete-mapping.js +0 -13
- package/lib/decorator/get-mapping.d.ts +0 -1
- package/lib/decorator/get-mapping.js +0 -13
- package/lib/decorator/ignore-residual-params.d.ts +0 -1
- package/lib/decorator/ignore-residual-params.js +0 -24
- package/lib/decorator/patch-mapping.d.ts +0 -1
- package/lib/decorator/patch-mapping.js +0 -13
- package/lib/decorator/post-mapping.d.ts +0 -1
- package/lib/decorator/post-mapping.js +0 -13
- package/lib/decorator/put-mapping.d.ts +0 -1
- package/lib/decorator/put-mapping.js +0 -13
- package/plugins/auth/index.d.ts +0 -4
- package/plugins/auth/index.js +0 -26
package/README.md
CHANGED
|
@@ -1,858 +1,525 @@
|
|
|
1
|
-
#
|
|
1
|
+
# axios-annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="left">
|
|
4
|
+
<a href="./README_EN.md">English</a> | <b>简体中文</b>
|
|
5
|
+
</p>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/axios-annotations)
|
|
8
|
+
[](https://github.com/sitorhy/axios-annotations/blob/master/LICENSE)
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
---
|
|
11
|
+
Quick Configuration Framework for Axios with TypeScript support.
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
+ Step 3:构建服务实例,调用接口
|
|
13
|
+
> 声明式`API`配置工具。将 **请求的构建声明** 与 **业务数据的提供** 完全分离。不再手动拼装 `URL`
|
|
14
|
+
、请求头和请求体,而是通过装饰器来“声明”一个请求应该如何被构建。
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
# 装饰器可用范围
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
| 装饰器 | 可用范围 | 描述 |
|
|
19
|
+
|:------------------|:-------|:--------------------------|
|
|
20
|
+
| `@RequestConfig` | 类 / 方法 | 设置请求配置(例如 `signal`)。 |
|
|
21
|
+
| `@RequestMapping` | 类 / 方法 | 定义 `URL` 路径和 `HTTP` 请求方法。 |
|
|
22
|
+
| `@RequestWith` | 方法 | 重定向请求方法使用的配置。 |
|
|
23
|
+
| `@RequestBody` | 方法 | 标记一个参数作为请求体。 |
|
|
24
|
+
| `@RequestHeader` | 方法 | 标记一个参数作为请求头。 |
|
|
25
|
+
| `@RequestParam` | 方法 | 标记一个参数作为 `URL` 查询参数。 |
|
|
26
|
+
| `@PathVariables` | 方法 | 标记一个参数作为 `URL` 路径变量。 |
|
|
16
27
|
|
|
17
|
-
|
|
18
|
-
import {config, Service} from "axios-annotations"
|
|
19
|
-
|
|
20
|
-
config.protocol = "http";
|
|
21
|
-
config.host = "localhost";
|
|
22
|
-
config.port = 8080;
|
|
23
|
-
config.prefix = "/api";
|
|
24
|
-
|
|
25
|
-
export default class TestService extends Service {
|
|
26
|
-
/**
|
|
27
|
-
* new TestService().get("a","b",null);
|
|
28
|
-
* <br>
|
|
29
|
-
* http://localhost:8080/api/path?p1=a&p2=b
|
|
30
|
-
* @param data1
|
|
31
|
-
* @param data2
|
|
32
|
-
* @returns {AxiosPromise<any>}
|
|
33
|
-
*/
|
|
34
|
-
get(required1, required2, optional1) {
|
|
35
|
-
return this.requestWith("GET", "/path")
|
|
36
|
-
.param("p1", true)
|
|
37
|
-
.param("p2", true)
|
|
38
|
-
.param("p3", false)
|
|
39
|
-
.send({
|
|
40
|
-
p1: required1,
|
|
41
|
-
p2: required2,
|
|
42
|
-
p3: optional1
|
|
43
|
-
});
|
|
44
|
-
}
|
|
28
|
+
---
|
|
45
29
|
|
|
46
|
-
|
|
47
|
-
return this.requestWith("POST", "/path2")
|
|
48
|
-
.param("p1", true)
|
|
49
|
-
.body("p2")
|
|
50
|
-
.send({
|
|
51
|
-
p1: data1,
|
|
52
|
-
p2: data2
|
|
53
|
-
});
|
|
54
|
-
}
|
|
30
|
+
# Usage
|
|
55
31
|
|
|
56
|
-
|
|
57
|
-
return this.requestWith("GET", "/path3/{id}")
|
|
58
|
-
.send({
|
|
59
|
-
id
|
|
60
|
-
});
|
|
61
|
-
}
|
|
32
|
+
## 流程简述
|
|
62
33
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'Content-Type': 'application/json'
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
剩下自由发挥,自行管理服务实例。
|
|
34
|
+
+ **声明式构建**:使用 `@RequestMapping`, `@RequestParam` 等装饰器来描述一个请求的静态结构。
|
|
35
|
+
+ **提供数据源**:业务方法接收参数,并 `return` 一个由这些参数组装成的普通 `JavaScript`
|
|
36
|
+
对象。这个对象将作为所有装饰器的数据源 (`Data Source`)。
|
|
37
|
+
+ **自动执行**:框架会拦截方法调用,使用装饰器定义的规则,从返回的数据源对象中提取数据,自动构建并执行请求。
|
|
76
38
|
|
|
77
|
-
|
|
78
|
-
const ApiCommon = {
|
|
79
|
-
test: new TestService()
|
|
80
|
-
};
|
|
39
|
+
## 创建配置
|
|
81
40
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
41
|
+
框架内置默认的全局配置,默认指向本地的`8080`端口,但建议自行创建和管理配置。
|
|
42
|
+
例:
|
|
85
43
|
|
|
44
|
+
```ts
|
|
45
|
+
// config.ts
|
|
46
|
+
import {Config} from "axios-annotations";
|
|
86
47
|
|
|
48
|
+
export const localConfig = new Config({
|
|
49
|
+
protocol: 'http',
|
|
50
|
+
host: 'localhost',
|
|
51
|
+
prefix: '', // 请求前缀,可选
|
|
52
|
+
port: 5173
|
|
53
|
+
});
|
|
54
|
+
```
|
|
87
55
|
|
|
88
|
-
|
|
56
|
+
通过 `@RequestConfig` 注入服务类,注意服务类需要继承 `Service`。
|
|
89
57
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<br>
|
|
94
|
-
`@babel/plugin-proposal-decorators`
|
|
95
|
-
<br>
|
|
96
|
-
`@babel/plugin-proposal-class-properties`
|
|
97
|
-
<br>
|
|
98
|
-
添加配置:
|
|
58
|
+
```ts
|
|
59
|
+
import {localConfig} from "./config.ts";
|
|
60
|
+
import {Service, RequestConfig} from "axios-annotations";
|
|
99
61
|
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
[
|
|
104
|
-
"@babel/plugin-proposal-decorators",
|
|
105
|
-
{
|
|
106
|
-
"legacy": true
|
|
107
|
-
}
|
|
108
|
-
],
|
|
109
|
-
[
|
|
110
|
-
"@babel/plugin-proposal-class-properties",
|
|
111
|
-
{
|
|
112
|
-
"loose": true
|
|
113
|
-
}
|
|
114
|
-
]
|
|
115
|
-
]
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
tsconfig.json / jsconfig.json
|
|
119
|
-
```json
|
|
120
|
-
{
|
|
121
|
-
"compilerOptions": {
|
|
122
|
-
"experimentalDecorators": true,
|
|
123
|
-
"emitDecoratorMetadata": true
|
|
124
|
-
}
|
|
62
|
+
@RequestConfig(localConfig)
|
|
63
|
+
export class DemoService extends Service {
|
|
64
|
+
// ...
|
|
125
65
|
}
|
|
126
66
|
```
|
|
127
67
|
|
|
128
|
-
`
|
|
129
|
-
<br>接口方法只需要处理和返回参数,并注解参数类型,框架根据注解分拆参数并注入`HTTP`请求。
|
|
68
|
+
请求前缀可以在 `Config` 的 `prefix` 中指定,或者使用 `@RequestMapping` 指定前缀。
|
|
130
69
|
|
|
131
|
-
|
|
132
|
-
import {
|
|
133
|
-
Service,
|
|
134
|
-
RequestConfig,
|
|
135
|
-
RequestParam,
|
|
136
|
-
RequestMapping,
|
|
137
|
-
RequestBody,
|
|
138
|
-
RequestHeader,
|
|
139
|
-
IgnoreResidualParams
|
|
140
|
-
} from "axios-annotations";
|
|
70
|
+
## @RequestMapping
|
|
141
71
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
get(p1, p2, p3) {
|
|
149
|
-
return {p1, p2, p3};
|
|
150
|
-
}
|
|
72
|
+
+ 声明服务使用的统一请求前缀。
|
|
73
|
+
+ 声明业务方法的请求方法和请求路径,注解方法的时候第二参数必填。
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import {localConfig} from "./config.ts";
|
|
77
|
+
import {Expect, Service, RequestConfig, RequestMapping} from "axios-annotations";
|
|
151
78
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return
|
|
79
|
+
@RequestConfig(localConfig)
|
|
80
|
+
@RequestMapping("/api")
|
|
81
|
+
export class DemoService extends Service {
|
|
82
|
+
// [GET] http://localhost:5173/api/foo
|
|
83
|
+
@RequestMapping("/foo", "GET")
|
|
84
|
+
foo() {
|
|
85
|
+
return Expect<Record<any>>({});
|
|
159
86
|
}
|
|
160
87
|
|
|
161
|
-
|
|
162
|
-
@
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
p1: "p1",
|
|
166
|
-
p2: "p2",
|
|
167
|
-
p3: "p3"
|
|
168
|
-
}
|
|
88
|
+
// [POST] http://localhost:5173/api/bar
|
|
89
|
+
@RequestMapping("/bar", "POST")
|
|
90
|
+
bar() {
|
|
91
|
+
return Expect<Record<any>>({});
|
|
169
92
|
}
|
|
170
93
|
}
|
|
171
94
|
```
|
|
172
95
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
```javascript
|
|
176
|
-
const ApiCommon = {
|
|
177
|
-
test: new TestService()
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// 调用API
|
|
181
|
-
ApiCommon.test.get("a","b",null);
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
如果不爽部分IDE的`non-promise inspection info`下划线,也可以给方法加上`async`。
|
|
187
|
-
|
|
188
|
-
### 代码提示
|
|
96
|
+
## @RequestWith
|
|
189
97
|
|
|
190
|
-
|
|
98
|
+
+ 重定向方法使用的配置,服务类的业务划分需要用到不同的服务器地址。
|
|
99
|
+
例子,请求不同服务器的文件:
|
|
191
100
|
|
|
192
|
-
|
|
101
|
+
```ts
|
|
102
|
+
import {Config, Expect, Service, RequestConfig, RequestMapping, RequestWith} from "axios-annotations";
|
|
193
103
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```typescript
|
|
201
|
-
import {Expect} from "axios-annotations";
|
|
104
|
+
const config = new Config({
|
|
105
|
+
protocol: "http",
|
|
106
|
+
host: "localhost",
|
|
107
|
+
prefix: "/resources",
|
|
108
|
+
port: 5173
|
|
109
|
+
});
|
|
202
110
|
|
|
203
|
-
|
|
111
|
+
const config2 = new Config({
|
|
112
|
+
protocol: "http",
|
|
113
|
+
host: "localhost",
|
|
114
|
+
prefix: "/data",
|
|
115
|
+
port: 5173
|
|
116
|
+
});
|
|
204
117
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@RequestMapping("/
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return Expect<{
|
|
211
|
-
|
|
212
|
-
success: boolean;
|
|
213
|
-
}>({
|
|
214
|
-
body: message
|
|
118
|
+
@RequestConfig(config)
|
|
119
|
+
export class FileService extends Service {
|
|
120
|
+
@RequestMapping("/test1.json", "GET")
|
|
121
|
+
getFile() {
|
|
122
|
+
// [GET] http://localhost:5173/resources/test1.json
|
|
123
|
+
return Expect<any>({
|
|
124
|
+
params: {}
|
|
215
125
|
});
|
|
216
126
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// call method, the return value can be deconstructed by IDE
|
|
220
|
-
|
|
221
|
-
const res: AxiosResponse<{
|
|
222
|
-
data:string;
|
|
223
|
-
success:boolean;
|
|
224
|
-
}> = await new TestService().postMessage("foo");
|
|
225
|
-
|
|
226
|
-
console.log(res.data.data);
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
|
|
230
127
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
import qs from "qs";
|
|
239
|
-
import {URLSearchParamsParser} from "axios-annotations";
|
|
240
|
-
|
|
241
|
-
if (typeof URLSearchParams === "undefined") {
|
|
242
|
-
URLSearchParamsParser.encode = function (encoder) {
|
|
243
|
-
return qs.stringify(encoder);
|
|
128
|
+
@RequestWith(config2)
|
|
129
|
+
@RequestMapping("/test2.json", "GET")
|
|
130
|
+
getFile2() {
|
|
131
|
+
// [GET] http://localhost:5173/data/test2.json
|
|
132
|
+
return Expect<any>({
|
|
133
|
+
params: {}
|
|
134
|
+
});
|
|
244
135
|
}
|
|
245
136
|
}
|
|
246
137
|
```
|
|
247
138
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
### Custom Config
|
|
139
|
+
注意,如果服务类使用 `@RequestMapping` 声明了统一前缀,那么使用 `@RequestWith` 重定向仍然会拼接服务类的统一前缀。
|
|
140
|
+
如果需要服务类方法使用不同的请求前缀,你可能需要将服务的统一前缀提升到 `Config` 的 `prefix` 字段或者取消前缀相关配置。
|
|
251
141
|
|
|
252
|
-
|
|
142
|
+
```ts
|
|
143
|
+
// ...config
|
|
253
144
|
|
|
254
|
-
|
|
255
|
-
import {
|
|
256
|
-
Config,
|
|
257
|
-
RequestConfig,
|
|
258
|
-
RequestMapping
|
|
259
|
-
} from "axios-annotations";
|
|
260
|
-
|
|
261
|
-
const config = new Config({
|
|
262
|
-
host: "localhost",
|
|
263
|
-
port: 8086,
|
|
145
|
+
const config2 = new Config({
|
|
264
146
|
protocol: "http",
|
|
265
|
-
|
|
266
|
-
|
|
147
|
+
host: "localhost",
|
|
148
|
+
prefix: "/data",
|
|
149
|
+
port: 5173
|
|
267
150
|
});
|
|
268
151
|
|
|
269
152
|
@RequestConfig(config)
|
|
270
|
-
@RequestMapping("/
|
|
271
|
-
export
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
153
|
+
@RequestMapping("/api")
|
|
154
|
+
export class FileService extends Service {
|
|
155
|
+
// ...
|
|
156
|
+
|
|
157
|
+
@RequestWith(config2)
|
|
158
|
+
@RequestMapping("/test2.json", "GET")
|
|
159
|
+
getFile2() {
|
|
160
|
+
// [GET] http://localhost:5173/data/api/test2.json
|
|
161
|
+
return Expect<any>({
|
|
162
|
+
params: {}
|
|
163
|
+
});
|
|
275
164
|
}
|
|
276
165
|
}
|
|
277
166
|
```
|
|
278
167
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
All Services inject this by default.
|
|
168
|
+
## @RequestBody
|
|
282
169
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
config.host = "192.168.137.1";
|
|
287
|
-
config.port = 8080;
|
|
288
|
-
// ...
|
|
289
|
-
```
|
|
170
|
+
+ 仅用于声明请求体的取值字段,默认不传则为 `"body"` 字段, `@RequestBody()` 跟 `@RequestBody("body")` 等价:
|
|
171
|
+
注意请求体因类型问题无法合并,多次声明请求体取值以最后执行的为准。
|
|
290
172
|
|
|
291
|
-
|
|
173
|
+
```ts
|
|
292
174
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
```javascript
|
|
308
|
-
@RequestConfig(new Config({
|
|
309
|
-
protocol: "http",
|
|
310
|
-
host: "localhost",
|
|
311
|
-
port: 8888,
|
|
312
|
-
prefix: "/prefix"
|
|
313
|
-
}))
|
|
314
|
-
@RequestMapping("/oauth")
|
|
315
|
-
class AuthService extends Service {
|
|
316
|
-
@PostMapping("/login")
|
|
317
|
-
@RequestWith("withoutAuth")
|
|
318
|
-
login() {
|
|
319
|
-
// http://localhost:9999/auth/oauth/login
|
|
320
|
-
return {usename: "0x123456", password: "123456"};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
@GetMapping("/foo")
|
|
324
|
-
bar() {
|
|
325
|
-
// http://localhost:8888/prefix/oauth/foo
|
|
326
|
-
return {};
|
|
175
|
+
@RequestConfig(config)
|
|
176
|
+
@RequestMapping("/api")
|
|
177
|
+
export class FileService extends Service {
|
|
178
|
+
// ...
|
|
179
|
+
@RequestMapping("/files/package_graph.json", "POST")
|
|
180
|
+
@RequestBody()
|
|
181
|
+
getPackageGraph() {
|
|
182
|
+
return Expect<Record<string, any>>({
|
|
183
|
+
body: {
|
|
184
|
+
version: '3.x',
|
|
185
|
+
description: '示例数据'
|
|
186
|
+
}
|
|
187
|
+
});
|
|
327
188
|
}
|
|
328
189
|
}
|
|
329
190
|
```
|
|
330
191
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
### 静态注入
|
|
192
|
+
`@RequestBody` 的扩展写法,用于指定静态值或自定义取值逻辑。
|
|
193
|
+
指定静态值:
|
|
334
194
|
|
|
335
|
-
|
|
195
|
+
```ts
|
|
336
196
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// ...
|
|
340
|
-
class AuthService extends Service {
|
|
197
|
+
@RequestConfig(config)
|
|
198
|
+
export class FileService extends Service {
|
|
341
199
|
// ...
|
|
342
|
-
@
|
|
343
|
-
|
|
200
|
+
@RequestMapping("/files/package_graph.json", "POST")
|
|
201
|
+
@RequestBody({
|
|
202
|
+
value: {
|
|
203
|
+
version: '3.x',
|
|
204
|
+
description: '示例数据'
|
|
205
|
+
}
|
|
344
206
|
})
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
return {};
|
|
207
|
+
getPackageGraph() {
|
|
208
|
+
return Expect<Record<string, any>>({});
|
|
348
209
|
}
|
|
349
210
|
}
|
|
350
|
-
|
|
351
|
-
// 取消请求
|
|
352
|
-
controller.abort()
|
|
353
211
|
```
|
|
354
212
|
|
|
355
|
-
|
|
213
|
+
自定义取值逻辑:
|
|
356
214
|
|
|
357
|
-
```
|
|
358
|
-
const controller = new AbortController();
|
|
359
|
-
|
|
360
|
-
// ...
|
|
361
|
-
class AuthService extends Service {
|
|
362
|
-
// ...
|
|
363
|
-
@AbortSource(controller)
|
|
364
|
-
bar() {
|
|
365
|
-
// ....
|
|
366
|
-
return {};
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
new AuthService().bar().then(() => {
|
|
371
|
-
|
|
372
|
-
}).catch(e => {
|
|
373
|
-
console.log(axios.isCancel(e));
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// 取消请求
|
|
377
|
-
controller.abort()
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
兼容旧版 `CancelToken` `(deprecated)`:
|
|
381
|
-
```javascript
|
|
382
|
-
const CancelToken = axios.CancelToken;
|
|
383
|
-
|
|
384
|
-
const controller = new AbortControllerAdapter(CancelToken);
|
|
385
|
-
|
|
386
|
-
// ...
|
|
387
|
-
class AuthService extends Service {
|
|
388
|
-
// ...
|
|
389
|
-
@AbortSource(controller)
|
|
390
|
-
bar() {
|
|
391
|
-
// ....
|
|
392
|
-
return {};
|
|
393
|
-
}
|
|
394
|
-
}
|
|
215
|
+
```ts
|
|
395
216
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
// ...
|
|
399
|
-
}).catch(e => {
|
|
400
|
-
console.log(axios.isCancel(e));
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// 取消请求
|
|
404
|
-
controller.signal.onabort = () => {
|
|
405
|
-
console.log("aborted");
|
|
406
|
-
};
|
|
407
|
-
controller.abort("cancel test")
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### 动态创建中断源
|
|
411
|
-
不确定中断时机,自由发挥。
|
|
412
|
-
```javascript
|
|
413
|
-
// 自定义中断逻辑
|
|
414
|
-
class AbortSourceManager {
|
|
415
|
-
// ...
|
|
416
|
-
|
|
417
|
-
create () {
|
|
418
|
-
return AbortController();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
abortAll () {
|
|
422
|
-
// ...
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const manager = new AbortSourceManager();
|
|
427
|
-
|
|
428
|
-
// ...
|
|
429
|
-
class AuthService extends Service {
|
|
217
|
+
@RequestConfig(config)
|
|
218
|
+
export class FileService extends Service {
|
|
430
219
|
// ...
|
|
431
|
-
@
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
220
|
+
@RequestMapping("/files/package_graph.json", "POST")
|
|
221
|
+
@RequestBody({
|
|
222
|
+
value: function () {
|
|
223
|
+
return {
|
|
224
|
+
version: '3.x',
|
|
225
|
+
description: '示例数据2'
|
|
226
|
+
};
|
|
227
|
+
}
|
|
438
228
|
})
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
return {};
|
|
229
|
+
getPackageGraph() {
|
|
230
|
+
return Expect<Record<string, any>>({});
|
|
442
231
|
}
|
|
443
232
|
}
|
|
444
233
|
```
|
|
445
234
|
|
|
235
|
+
## @RequestHeader
|
|
446
236
|
|
|
447
|
-
|
|
237
|
+
声明请求头的取值,有以下的取值方式:
|
|
448
238
|
|
|
449
|
-
|
|
239
|
+
+ 声明数据源字段,`@RequestHeader(key: string, required?: boolean = true)` 第二参数`required`默认必填,`value`为空值时(`null` / `undefined` /`''`)默认转为空字符串。
|
|
240
|
+
+ 声明静态值,`key`必填。
|
|
241
|
+
+ 声明自定义取值逻辑,`key`必填。
|
|
242
|
+
请求头取值可合并,所以这里一次性给出取值示例:
|
|
450
243
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
插件在`Config`对象的`axios`实例创建时注入,建议在`Config`构造函数配置。
|
|
244
|
+
```ts
|
|
454
245
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
246
|
+
@RequestConfig(config)
|
|
247
|
+
export class FileService extends Service {
|
|
248
|
+
// ...
|
|
249
|
+
// 声明数据源字段,且必填
|
|
250
|
+
@RequestHeader("Custom-Header", true)
|
|
251
|
+
// 声明静态值
|
|
252
|
+
@RequestHeader({
|
|
253
|
+
key: "Custom-Header-2",
|
|
254
|
+
value: "static-header-value",
|
|
255
|
+
required: true,
|
|
256
|
+
})
|
|
257
|
+
// 声明自定义取值逻辑
|
|
258
|
+
@RequestHeader({
|
|
259
|
+
key: "Custom-Header-3",
|
|
260
|
+
value: function (source: Record<string, any>) {
|
|
261
|
+
return source.num1 + source.num2;
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
getPackageGraph() {
|
|
265
|
+
return Expect<Record<string, any>>({
|
|
266
|
+
"Custom-Header": "header-value-from-source",
|
|
267
|
+
num1: 100,
|
|
268
|
+
num2: 200
|
|
470
269
|
});
|
|
471
270
|
}
|
|
472
271
|
}
|
|
473
272
|
```
|
|
474
273
|
|
|
475
|
-
|
|
274
|
+
生成的请求头为:
|
|
476
275
|
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (typeof wx !== "undefined") {
|
|
482
|
-
wx.showToast({
|
|
483
|
-
icon: "none",
|
|
484
|
-
title: `[${e.response.status}]` + ' ' + e.config.url
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
})
|
|
488
|
-
]
|
|
489
|
-
})
|
|
276
|
+
```
|
|
277
|
+
Custom-Header: header-value-from-source
|
|
278
|
+
Custom-Header-2: static-header-value
|
|
279
|
+
Custom-Header-3: 300
|
|
490
280
|
```
|
|
491
281
|
|
|
282
|
+
## @RequestParam
|
|
492
283
|
|
|
284
|
+
声明查询参数的取值,取值方式用法跟请求头的声明类似:
|
|
493
285
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
<br>
|
|
498
|
-
Basic Usage for Auth Plugin.
|
|
499
|
-
<br>
|
|
500
|
-
Take the case of `Spring Security OAtuh2.0`。
|
|
286
|
+
+ 声明数据源字段,`@RequestParam(key: string, required?: boolean = false)` 第二参数默认选填,空值不会拼接到请求地址。
|
|
287
|
+
+ 声明静态值, `key` 必填。
|
|
288
|
+
+ 声明自定义取值逻辑, `key` 必填。
|
|
501
289
|
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
const authCfg = new Config({
|
|
505
|
-
host: "localhost",
|
|
506
|
-
port: 8080,
|
|
290
|
+
```ts
|
|
291
|
+
const config = new Config({
|
|
507
292
|
protocol: "http",
|
|
508
|
-
|
|
293
|
+
host: "localhost",
|
|
294
|
+
prefix: "/resources",
|
|
295
|
+
port: 5173
|
|
509
296
|
});
|
|
510
297
|
|
|
511
|
-
@RequestConfig(
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
@
|
|
515
|
-
@RequestParam("
|
|
516
|
-
|
|
517
|
-
@RequestParam(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
@RequestBody()
|
|
522
|
-
token() {
|
|
523
|
-
return {
|
|
524
|
-
grant_type: "password",
|
|
525
|
-
scope: "all",
|
|
526
|
-
client_id: "client_1",
|
|
527
|
-
client_secret: "123456",
|
|
528
|
-
username: "admin",
|
|
529
|
-
password: "123456"
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
@GetMapping("/token")
|
|
534
|
-
@RequestParam("grant_type", true)
|
|
535
|
-
@RequestParam("refresh_token", true)
|
|
536
|
-
@RequestParam("scope", false)
|
|
537
|
-
@RequestParam("client_id", true)
|
|
538
|
-
@RequestParam("client_secret", true)
|
|
539
|
-
refreshToken(session) {
|
|
540
|
-
return {
|
|
541
|
-
grant_type: "refresh_token",
|
|
542
|
-
refresh_token: session.refresh_token,
|
|
543
|
-
scope: "all",
|
|
544
|
-
client_id: "client_1",
|
|
545
|
-
client_secret: "123456"
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
Implement Authorizer.
|
|
552
|
-
<br/>
|
|
553
|
-
实现`Authorizer`类。至少需要实现方法`refreshSession`、`onAuthorizedDenied`。如果需要调用`invalidateSession`,还需要重载`onSessionInvalidated`。
|
|
554
|
-
|
|
555
|
-
```javascript
|
|
556
|
-
import {Authorizer} from "axios-annotations/plugins/auth";
|
|
557
|
-
|
|
558
|
-
export default class OAuth2Authorizer extends Authorizer {
|
|
559
|
-
async refreshSession(session) {
|
|
560
|
-
// access_token invalid, could refresh access_token with refresh_token through 'password' grant type
|
|
561
|
-
// access_token 过期,如果使用 password 方式认证, 可使用 refresh_token 进行刷新
|
|
562
|
-
const oauthService = new OAuth2Service();
|
|
563
|
-
let res;
|
|
564
|
-
try {
|
|
565
|
-
res = await oauthService.refreshToken(session);
|
|
566
|
-
} catch (e) {
|
|
567
|
-
throw e;
|
|
568
|
-
}
|
|
569
|
-
if (!res || !res.data) {
|
|
570
|
-
throw new Error("Seession Unknow Error");
|
|
571
|
-
}
|
|
572
|
-
const nextSession = res.data;
|
|
573
|
-
return nextSession;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
async onAuthorizedDenied(error) {
|
|
577
|
-
// refresh_token invalid (HTTP 401 default),you should re-loign or logout here.
|
|
578
|
-
// refresh_token 过期触发该回调,在此进行重新登录或注销操作
|
|
579
|
-
|
|
580
|
-
// try logout, clean session.
|
|
581
|
-
// await this.invalidateSession();
|
|
582
|
-
// return;
|
|
583
|
-
|
|
584
|
-
const res = await new OAuth2Service().token();
|
|
585
|
-
if (res && res.data) {
|
|
586
|
-
const nextSession = res.data;
|
|
587
|
-
|
|
588
|
-
// save session manually if try re-login
|
|
589
|
-
await this.storageSession(nextSession);
|
|
590
|
-
return nextSession;
|
|
298
|
+
@RequestConfig(config)
|
|
299
|
+
export class FileService extends Service {
|
|
300
|
+
@RequestMapping("/demo.json", "POST")
|
|
301
|
+
@RequestParam("param1") // 基本形式
|
|
302
|
+
@RequestParam("param2") // 基本形式
|
|
303
|
+
// 扩展写法(函数)
|
|
304
|
+
@RequestParam({
|
|
305
|
+
key: "sum",
|
|
306
|
+
value: function (source: Record<string, any>) {
|
|
307
|
+
return Number(source['param1']) + Number(source['param2']);
|
|
591
308
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
309
|
+
})
|
|
310
|
+
// 扩展写法(静态值)
|
|
311
|
+
@RequestParam({
|
|
312
|
+
key: 'static',
|
|
313
|
+
value: 'foo'
|
|
314
|
+
})
|
|
315
|
+
@RequestBody()
|
|
316
|
+
getJson() {
|
|
317
|
+
return Expect<Record<string, any>>({
|
|
318
|
+
param1: '114',
|
|
319
|
+
param2: '514',
|
|
320
|
+
body: {
|
|
321
|
+
employees: [
|
|
322
|
+
{
|
|
323
|
+
firstName: "John",
|
|
324
|
+
lastName: "Doe"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
});
|
|
600
329
|
}
|
|
601
330
|
}
|
|
602
331
|
```
|
|
603
332
|
|
|
604
|
-
|
|
605
|
-
<br>
|
|
333
|
+
调用 `getJson` 生成的地址:
|
|
606
334
|
|
|
607
|
-
|
|
335
|
+
```text
|
|
336
|
+
http://localhost:5173/resources/demo.json?static=foo&sum=628¶m2=514¶m1=114
|
|
337
|
+
```
|
|
608
338
|
|
|
609
|
-
|
|
610
|
-
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
611
|
-
import {SessionStorage} from "axios-annotations/plugins/auth";
|
|
339
|
+
## @PathVariables
|
|
612
340
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// key: default "$_SESSION"
|
|
617
|
-
await AsyncStorage.setItem(key, jsonValue);
|
|
618
|
-
}
|
|
341
|
+
启用路径参数,从数据源或数据源字段中取值,取值必须为 `PlainObject`,替换 `url` 中的占位符。
|
|
342
|
+
启用路径参数请至少确保有一个 `@PathVariables` 声明,不指定路径参数数据源字段时,直接从数据源本体取值。
|
|
343
|
+
由于路径参数取值(`PlainObject`)可合并,此处给出所有取值示例:
|
|
619
344
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
345
|
+
```ts
|
|
346
|
+
export const localConfig = new Config({
|
|
347
|
+
protocol: 'http',
|
|
348
|
+
host: 'localhost',
|
|
349
|
+
port: 5173
|
|
350
|
+
});
|
|
623
351
|
|
|
624
|
-
|
|
625
|
-
|
|
352
|
+
@RequestConfig(config)
|
|
353
|
+
export class FileService extends Service {
|
|
354
|
+
@RequestMapping("/files/{fileName}?a={a}&c={c}&e={e}", "GET")
|
|
355
|
+
@PathVariables() // 不指定字段,从数据源本体取值,此处会查询出 "fileName"
|
|
356
|
+
@PathVariables({
|
|
357
|
+
// 自定义数据源生成逻辑
|
|
358
|
+
value: function (source: Record<string, any>) {
|
|
359
|
+
return {
|
|
360
|
+
a: 100,
|
|
361
|
+
c: 300,
|
|
362
|
+
d: source.d
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
// 此处 pathVariablesKey 的值会跟数据源本体合并为一个新的对象,所以会查询出 "e"
|
|
367
|
+
@PathVariables('pathVariablesKey') // => "e"
|
|
368
|
+
// 注意这个是查询参数,生成逻辑跟路径参数不一样
|
|
369
|
+
@RequestParam({
|
|
370
|
+
key: 'b',
|
|
371
|
+
value: 200
|
|
372
|
+
})
|
|
373
|
+
getFileInfo(fileName: string) {
|
|
374
|
+
return Expect<Record<string, any>>({
|
|
375
|
+
fileName,
|
|
376
|
+
d: 400,
|
|
377
|
+
pathVariablesKey: {
|
|
378
|
+
e: 500
|
|
379
|
+
}
|
|
380
|
+
});
|
|
626
381
|
}
|
|
627
382
|
}
|
|
628
383
|
```
|
|
629
384
|
|
|
630
|
-
|
|
385
|
+
路径参数可用于查询参数的填充,但跟 `@RequestParam` 有区别:
|
|
631
386
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
this.sessionStorage = new RNSessionStorage();
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
```
|
|
387
|
+
+ `@RequestParam` 合并值直接传给 `axios` 的 `params` 参数,对于数组等复合类型,使用 `axios` 的生成逻辑,路径参数则直接转为
|
|
388
|
+
`JSON` 字符串,所以不要传非基本类型。
|
|
389
|
+
+ 占位符是固定的字符序列,路径参数没有选填逻辑。
|
|
390
|
+
+ 转换非基本类型失败一律转为 `"undefined"`。
|
|
640
391
|
|
|
641
|
-
|
|
392
|
+
## @RequestConfig
|
|
642
393
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
import {Config} from "axios-annotations";
|
|
646
|
-
import {AuthorizationPlugin} from "axios-annotations/plugins/auth";
|
|
647
|
-
|
|
648
|
-
const _authorizer = new OAuth2Authorizer();
|
|
394
|
+
+ 注解类时,用于设置服务类使用的配置。
|
|
395
|
+
+ 注解方法时,合并 `AxiosRequestConfig` 配置,例如有 `N` 个查询参数/请求头不想一个个配置,可以使用 `@RequestConfig` 统一返回。
|
|
649
396
|
|
|
397
|
+
```ts
|
|
650
398
|
const config = new Config({
|
|
651
|
-
host: "localhost",
|
|
652
|
-
port: 8080,
|
|
653
399
|
protocol: "http",
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
]
|
|
400
|
+
host: "localhost",
|
|
401
|
+
prefix: "/data",
|
|
402
|
+
port: 5173
|
|
658
403
|
});
|
|
659
404
|
|
|
660
|
-
// export it in order to save or read the grant result
|
|
661
|
-
// 导出authorizer对象,方便读取或保存认证信息
|
|
662
|
-
export const authorizer = _authorizer;
|
|
663
|
-
|
|
664
|
-
// service.js
|
|
665
405
|
@RequestConfig(config)
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
406
|
+
export class FileService extends Service {
|
|
407
|
+
@RequestMapping("/test1.json", "GET")
|
|
408
|
+
@RequestConfig({
|
|
409
|
+
headers: {
|
|
410
|
+
'X-Source': 'class',
|
|
411
|
+
'Authorization': 'Bearer token'
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
@RequestConfig(function (source: Record<string, any>) {
|
|
415
|
+
return {
|
|
416
|
+
headers: {
|
|
417
|
+
'Token1': '1'
|
|
418
|
+
},
|
|
419
|
+
params: source.params
|
|
420
|
+
};
|
|
421
|
+
})
|
|
422
|
+
getData() {
|
|
423
|
+
// http://localhost:5173/data/test1.json?a=1&b=2
|
|
424
|
+
// ----
|
|
425
|
+
// Headers:
|
|
426
|
+
//
|
|
427
|
+
// X-Source: class
|
|
428
|
+
// Token1: 1
|
|
429
|
+
// Authorization: Bearer token
|
|
430
|
+
return Expect<Record<string, any>>({
|
|
431
|
+
params: {
|
|
432
|
+
'a': 1,
|
|
433
|
+
'b': 2
|
|
434
|
+
}
|
|
685
435
|
});
|
|
686
436
|
}
|
|
687
437
|
}
|
|
688
438
|
```
|
|
439
|
+
`@RequestConfig` 可用于注入 `AbortController` 或 `CancelToken` 中断源。
|
|
440
|
+
众所周知,`React 18+`组件在开发环境下会重复挂载和卸载(`<React.StrictMode>`),导致 `useEffect(() => {}, [])` 中的请求会发送两次。
|
|
441
|
+
此处举例如何限制发送一次请求,即组件卸载时候自动取消请求:
|
|
689
442
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
+ method : string `GET / POST / DELETE...`
|
|
697
|
-
+ path : string `相对路径`
|
|
698
|
-
+ data : Object `请求体`
|
|
699
|
-
+ config : Object `AxiosRequestConfig`
|
|
700
|
-
|
|
701
|
-
#### requestWith(method, path): RequestController
|
|
702
|
-
|
|
703
|
-
+ method : string `GET /POST / DELETE...`
|
|
704
|
-
+ path : string `相对路径`
|
|
705
|
-
|
|
706
|
-
> #### RequestController
|
|
707
|
-
> + param: (key, required?) : RequestController
|
|
708
|
-
> + key : string `标记查询串参数`
|
|
709
|
-
> + required : boolean `默认false,空字符串,null,undefined 将忽略`
|
|
710
|
-
> + header: (header, header) : RequestController
|
|
711
|
-
> + header : string `url 附加参数键值`
|
|
712
|
-
> + header : string | function `字符串,或者接收 send 方法参数的函数,该函数应返回合法值。`
|
|
713
|
-
> + body: (key) : RequestController
|
|
714
|
-
> + key : string `标记参数中请求体`
|
|
715
|
-
> + config: (cfg) : RequestController
|
|
716
|
-
> + cfg : `AxiosRequestConfig`
|
|
717
|
-
>
|
|
718
|
-
> + send: (data) : AxiosPromise<any>
|
|
719
|
-
> + data : object `参数键值对`
|
|
720
|
-
> + with: (name) : RequestController
|
|
721
|
-
> + name : string `config name : 已注册配置名称`
|
|
722
|
-
|
|
723
|
-
### Decorators
|
|
724
|
-
|
|
725
|
-
#### RequestMapping(path, method?)
|
|
726
|
-
|
|
727
|
-
+ path : string `相对路径`
|
|
728
|
-
+ method : string `默认GET,注解服务类时忽略该参数`
|
|
729
|
-
|
|
730
|
-
> 注解方法时,可以使用简化形式:
|
|
731
|
-
> <br>
|
|
732
|
-
> GetMapping(path)
|
|
733
|
-
> <br>
|
|
734
|
-
> PostMapping(path)
|
|
735
|
-
> <br>
|
|
736
|
-
> PatchMapping(path)
|
|
737
|
-
> <br>
|
|
738
|
-
> PutMapping(path)
|
|
739
|
-
> <br>
|
|
740
|
-
> DeleteMapping(path)
|
|
741
|
-
|
|
742
|
-
#### RequestParam(name, required?)
|
|
743
|
-
|
|
744
|
-
+ name : string `方法返回值属性`
|
|
745
|
-
+ required : boolean `是否必要参数`
|
|
746
|
-
|
|
747
|
-
#### RequestHeader(header, value)
|
|
748
|
-
|
|
749
|
-
+ header : string `请求头`
|
|
750
|
-
+ value : string `字符串或函数`
|
|
751
|
-
|
|
752
|
-
> 使用函数。
|
|
753
|
-
> ```javascript
|
|
754
|
-
> class TestService extends Service {
|
|
755
|
-
> @RequestHeader("Authorization", (token) => {
|
|
756
|
-
> return `Basic ${token}`;
|
|
757
|
-
> })
|
|
758
|
-
> @RequestMapping("/login", "GET")
|
|
759
|
-
> foo(token) {
|
|
760
|
-
> return {};
|
|
761
|
-
> }
|
|
762
|
-
> }
|
|
763
|
-
> ```
|
|
764
|
-
|
|
765
|
-
#### RequestBody(name)
|
|
766
|
-
|
|
767
|
-
+ name : string `方法返回值属性,默认为 body,不能与 RequestParam name 参数重复,如果重复 RequestBody 请使用别名`,
|
|
768
|
-
```javascript
|
|
769
|
-
class TestService extends Service {
|
|
770
|
-
@RequestMapping("/foo", "POST")
|
|
771
|
-
@RequestHeader("Content-Type", "text/plain")
|
|
772
|
-
@RequestParam("str", true)
|
|
773
|
-
@RequestParam("strRepeat", true)
|
|
774
|
-
foo(str) {
|
|
443
|
+
```ts
|
|
444
|
+
@RequestConfig(config)
|
|
445
|
+
export class FileService extends Service {
|
|
446
|
+
@RequestMapping("/test1.json", "GET")
|
|
447
|
+
@RequestConfig(function (source: Record<string, any>) {
|
|
775
448
|
return {
|
|
776
|
-
|
|
777
|
-
strRepeat: str // 如果请求体与查询串冲突
|
|
449
|
+
signal: source.signal
|
|
778
450
|
};
|
|
451
|
+
})
|
|
452
|
+
getData(signal: AbortSignal) {
|
|
453
|
+
return Expect<Record<string, any>>({
|
|
454
|
+
signal
|
|
455
|
+
});
|
|
779
456
|
}
|
|
780
457
|
}
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
#### IgnoreResidualParams(ignore?)
|
|
784
|
-
+ ignore : boolean `拼接 QueryString 时是否忽略没有声明的参数`
|
|
785
458
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
```javascript
|
|
791
|
-
// GET /foo?p1=p1&p2=p2&p3=p3
|
|
792
|
-
class TestService extends Service {
|
|
793
|
-
@RequestMapping("/foo", "GET")
|
|
794
|
-
@RequestParam("p1", true)
|
|
795
|
-
foo(token) {
|
|
796
|
-
return {
|
|
797
|
-
p1: "p1",
|
|
798
|
-
p2: "p2",
|
|
799
|
-
p3: "p3"
|
|
800
|
-
};
|
|
801
|
-
}
|
|
459
|
+
// 管理你的服务实例
|
|
460
|
+
export const ApiManager = {
|
|
461
|
+
fileService: new FileService()
|
|
802
462
|
}
|
|
803
463
|
```
|
|
804
|
-
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
464
|
+
`React`组件部分逻辑如下:
|
|
465
|
+
```tsx
|
|
466
|
+
import {ApiManager} from './api-manager'
|
|
467
|
+
import {useEffect} from "react";
|
|
468
|
+
|
|
469
|
+
export function ReactFunctionComponent() {
|
|
470
|
+
useEffect(function () {
|
|
471
|
+
const controller: AbortController = new AbortController();
|
|
472
|
+
ApiManager.fileService.getData(controller.signal);
|
|
473
|
+
|
|
474
|
+
return function () {
|
|
475
|
+
controller.abort();
|
|
476
|
+
};
|
|
477
|
+
}, []);
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div>
|
|
481
|
+
<p>AbortController Demo</p>
|
|
482
|
+
<p>开发环境下只有第二次挂载的请求会被接收</p>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
818
485
|
}
|
|
819
486
|
```
|
|
820
487
|
|
|
821
|
-
|
|
488
|
+
## Expect
|
|
822
489
|
|
|
823
|
-
|
|
490
|
+
函数原型参考:
|
|
824
491
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
```
|
|
829
|
-
+ getSession : Promise<Session> `获取授权信息`
|
|
830
|
-
```javascript
|
|
831
|
-
function onLogin(){
|
|
832
|
-
authorizer.getSession().then(session => {
|
|
833
|
-
// fetch other info / redirect to other page
|
|
834
|
-
store.dispatch('SAVE_USER_INFO' , info);
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
```
|
|
838
|
-
+ storageSession(session: Session): Promise<void> `存储授权信息`
|
|
839
|
-
+ checkResponse(response:AxiosResponse) : boolean `检查授权是否过期`
|
|
840
|
-
```javascript
|
|
841
|
-
class OAuth2Authorizer extends Authorizer {
|
|
842
|
-
checkResponse(response){
|
|
843
|
-
return response.stauts === 401; // default implement
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
```
|
|
492
|
+
```typescript
|
|
493
|
+
export default function Expect<T, D = AxiosPromise<T>>(params: any): D;
|
|
494
|
+
```
|
|
847
495
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
496
|
+
由于 `TypeScript` 的限制,装饰器无法在编译时改变一个方法的返回类型。方法在代码层面返回的是一个普通对象(数据源),但框架在运行时实际返回的是一个`AxiosPromise`。
|
|
497
|
+
`Expect<T>` 的作用就是解决这个“类型不匹配”的问题。它是一个类型桥梁,通过类型断言绕过静态检查,从而让你在调用代码时能够获得完整的类型安全和`IDE` 代码提示。
|
|
498
|
+
`Expect<T>` 的泛型 `T` 至关重要,它 **定义了你期望服务器响应体 `data` 的类型**。
|
|
499
|
+
|
|
500
|
+
- 如果接口返回一个具体的 `JSON` 对象,你应该为其定义一个接口 `MyData` 并使用 `Expect<MyData>(...)`。
|
|
501
|
+
|
|
502
|
+
- 如果接口返回纯文本,你应该使用 `Expect<string>(...)`。
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
## 不支持装饰器的环境
|
|
506
|
+
```ts
|
|
507
|
+
import {RequestBuilder} from "axios-annotations";
|
|
508
|
+
import {config} from './config';
|
|
509
|
+
|
|
510
|
+
async function foo() {
|
|
511
|
+
const response = await new RequestBuilder().param({
|
|
512
|
+
key: 'param',
|
|
513
|
+
value: function (source: Record<string, any>) {
|
|
514
|
+
return source.param;
|
|
515
|
+
}
|
|
516
|
+
}).buildWith(config, "/demo.json", "GET", {
|
|
517
|
+
param: 666
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
console.log(response.data);
|
|
521
|
+
}
|
|
522
|
+
```
|
|
856
523
|
|
|
857
524
|
## 运行环境
|
|
858
525
|
|
|
@@ -860,7 +527,7 @@ class TestService extends Service {
|
|
|
860
527
|
|
|
861
528
|
更新开发工具以支持装饰器语法。
|
|
862
529
|
|
|
863
|
-
小程序`Typescript`环境不支持装饰器编译,但是`Javascript`环境可以。把涉及到`API`配置的`*.ts`文件扩展名改为`*.js
|
|
530
|
+
小程序`Typescript`环境不支持装饰器编译,但是`Javascript`环境可以。把涉及到`API`配置的`*.ts`文件扩展名改为`*.js`,绕过部分环境对装饰器支持限制,本地配置勾选上`将JS编译成ES5`,正常引入即可。<br/>
|
|
864
531
|
|
|
865
532
|
> 开发工具BUG:`TS`环境`npm`构建失败
|
|
866
533
|
>
|
|
@@ -882,7 +549,7 @@ class TestService extends Service {
|
|
|
882
549
|
|
|
883
550
|
**安装第三方`axios`实现:**
|
|
884
551
|
|
|
885
|
-
+
|
|
552
|
+
+ 方案 1,使用适配器,`axios-miniprogram-adapter`
|
|
886
553
|
|
|
887
554
|
`axios`需要降级,版本再高就得报错:
|
|
888
555
|
|
|
@@ -890,38 +557,33 @@ class TestService extends Service {
|
|
|
890
557
|
npm install axios@0.26.1
|
|
891
558
|
npm install axios-miniprogram-adapter
|
|
892
559
|
```
|
|
893
|
-
|
|
560
|
+
|
|
894
561
|
开发工具如果编译报错 `module is not defined`, 在`app.js`头部补充缺失组件的声明:
|
|
895
|
-
|
|
562
|
+
|
|
896
563
|
```javascript
|
|
897
564
|
import {
|
|
898
565
|
Config,
|
|
899
|
-
URLSearchParamsParser,
|
|
900
|
-
AbortControllerAdapter,
|
|
901
566
|
Service,
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
GetMapping,
|
|
905
|
-
IgnoreResidualParams,
|
|
906
|
-
PatchMapping,
|
|
907
|
-
PostMapping,
|
|
908
|
-
PutMapping,
|
|
567
|
+
Expect,
|
|
568
|
+
AxiosStaticInstanceProvider,
|
|
909
569
|
RequestBody,
|
|
910
570
|
RequestConfig,
|
|
911
571
|
RequestHeader,
|
|
912
572
|
RequestMapping,
|
|
913
573
|
RequestParam,
|
|
914
|
-
|
|
574
|
+
PathVariables,
|
|
575
|
+
RequestWith,
|
|
576
|
+
RequestBuilder
|
|
915
577
|
} from "axios-annotations";
|
|
916
578
|
```
|
|
917
|
-
+
|
|
579
|
+
+ 方案 2,使用第三方实现,不限于`axios-miniprogram`
|
|
918
580
|
|
|
919
581
|
```shell
|
|
920
582
|
npm install axios-miniprogram
|
|
921
583
|
```
|
|
922
|
-
|
|
584
|
+
如果`npm`构建失败直接将`node_modules`下的目录复制出来。
|
|
923
585
|
实现`AxiosStaticInstanceProvider`并配置,如果`IDE`警告`provide`返回类型,可忽略掉:
|
|
924
|
-
|
|
586
|
+
|
|
925
587
|
```javascript
|
|
926
588
|
import mpAxios from 'axios-miniprogram';
|
|
927
589
|
|
|
@@ -940,5 +602,376 @@ class TestService extends Service {
|
|
|
940
602
|
axiosProvider: new ThirdAxiosStaticInstanceProvider(),
|
|
941
603
|
});
|
|
942
604
|
```
|
|
943
|
-
|
|
944
|
-
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# 插件
|
|
608
|
+
|
|
609
|
+
## 自定义插件
|
|
610
|
+
插件函数接收配置对象为参数,出于扩展性考虑,通常由高阶函数返回。
|
|
611
|
+
插件在`Config`对象的`axios`实例创建时注入,建议在`Config`构造函数配置。
|
|
612
|
+
```typescript
|
|
613
|
+
import {Config} from "axios-annotations"
|
|
614
|
+
import type {AxiosInstance} from "axios";
|
|
615
|
+
|
|
616
|
+
export function ToastPlugin(fnToast) {
|
|
617
|
+
return function (config: Config, axios: AxiosInstance) {
|
|
618
|
+
axios.interceptors.response.use(function (e) {
|
|
619
|
+
return Promise.resolve(e);
|
|
620
|
+
}, function (e) {
|
|
621
|
+
fnToast(e);
|
|
622
|
+
return Promise.reject(e);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
axios.interceptors.request.use(function (e) {
|
|
626
|
+
return Promise.resolve(e);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
配置插件:
|
|
632
|
+
```javascript
|
|
633
|
+
new Config({
|
|
634
|
+
plugins: [
|
|
635
|
+
ToastPlugin(function (e) {
|
|
636
|
+
if (typeof wx !== "undefined") {
|
|
637
|
+
wx.showToast({
|
|
638
|
+
icon: "none",
|
|
639
|
+
title: `[${e.response.status}]` + ' ' + e.config.url
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
]
|
|
644
|
+
})
|
|
645
|
+
```
|
|
646
|
+
## 授权插件 (可选)
|
|
647
|
+
使用该插件用于自动为请求写入授权信息(自动携带`Bearer Token`请求头等),默认适配`OAuth2`标准,直接给出示例:
|
|
648
|
+
+ 定义会话存储器
|
|
649
|
+
```ts
|
|
650
|
+
import {SessionStorage} from "axios-annotations/plugins/auth";
|
|
651
|
+
|
|
652
|
+
class WebSessionStorage extends SessionStorage {
|
|
653
|
+
async set(key: string, value: any): Promise<void> {
|
|
654
|
+
return sessionStorage.setItem(key, JSON.stringify(value));
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async get(key: string): Promise<any> {
|
|
658
|
+
const str = sessionStorage.getItem(key);
|
|
659
|
+
if (str) {
|
|
660
|
+
return JSON.parse(str);
|
|
661
|
+
} else {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async remove(key: string): Promise<void> {
|
|
667
|
+
return sessionStorage.removeItem(key);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
+ 实例化并导出`Authorizer`对象,并注入`SessionStorage`
|
|
672
|
+
> 这里导出的`Authorizer`对象将用于首次登录后会话信息写入(调用`storageSession`)和获取会话(`getSession`)。
|
|
673
|
+
```ts
|
|
674
|
+
import {Authorizer, type BasicSession} from "axios-annotations/plugins/auth";
|
|
675
|
+
|
|
676
|
+
class LocalAuthorizer extends Authorizer {
|
|
677
|
+
constructor() {
|
|
678
|
+
super();
|
|
679
|
+
this.sessionStorage = new WebSessionStorage();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 刷新 access_token,这里直接返回修改后的 session 对象即可
|
|
683
|
+
async refreshSession(session: BasicSession): Promise<any> {
|
|
684
|
+
const response = await new OAuthService().refreshToken({
|
|
685
|
+
refresh_token: session.refresh_token
|
|
686
|
+
});
|
|
687
|
+
console.log(`刷新 ${JSON.stringify(response.data)}`);
|
|
688
|
+
// 此处组装新的 session 对象并返回, onAuthorizedDenied 会捕获该方法运行时异常
|
|
689
|
+
return {
|
|
690
|
+
...response.data
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 附加 session 字段到请求头
|
|
695
|
+
withAuthentication(request: InternalAxiosRequestConfig, session: BasicSession) {
|
|
696
|
+
// 这里使用默认实现,如果 refreshSession 返回 null, withAuthentication 的逻辑不会执行
|
|
697
|
+
super.withAuthentication(request, session);
|
|
698
|
+
console.log(request.headers);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// 会话刷新过程抛出异常
|
|
702
|
+
// refresh_token 失效,约等于 refreshSession 返回 null
|
|
703
|
+
async onAuthorizedDenied(error: unknown): Promise<void> {
|
|
704
|
+
console.error(error); // 这个是 refreshSession 抛出的异常
|
|
705
|
+
|
|
706
|
+
// 删除会话信息
|
|
707
|
+
// 调用 invalidateSession 触发 onSessionInvalidated
|
|
708
|
+
await this.invalidateSession();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
onSessionInvalidated() {
|
|
712
|
+
// 跳转回登录页/首页
|
|
713
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export const authorizer = new LocalAuthorizer();
|
|
718
|
+
```
|
|
719
|
+
+ 定义授权链服务配置和业务链服务配置
|
|
720
|
+
授权链的接口应该跟业务链的接口分别独立`Service`,防止请求头冲突。
|
|
721
|
+
`OAuthService`示例代码适配`spring-boot-starter-oauth2-authorization-server 4.0.x`,仅供参考,看不懂可直接跳到`Authorizer`说明。
|
|
722
|
+
```ts
|
|
723
|
+
import AuthorizationPlugin, {type BasicSession} from "axios-annotations/plugins/auth";
|
|
724
|
+
|
|
725
|
+
const oauthConfig = new Config({
|
|
726
|
+
protocol: 'http',
|
|
727
|
+
host: 'localhost',
|
|
728
|
+
port: 8080
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// 这里业务接口跟授权接口使用同一服务器
|
|
732
|
+
const businessConfig = new Config({
|
|
733
|
+
protocol: 'http',
|
|
734
|
+
host: 'localhost',
|
|
735
|
+
port: 8080,
|
|
736
|
+
plugins: [
|
|
737
|
+
AuthorizationPlugin(authorizer)
|
|
738
|
+
]
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
@RequestConfig(oauthConfig)
|
|
742
|
+
export class OAuthService extends Service {
|
|
743
|
+
// 随机字符串
|
|
744
|
+
generateVerifier() {
|
|
745
|
+
const array = new Uint32Array(56);
|
|
746
|
+
window.crypto.getRandomValues(array);
|
|
747
|
+
return Array.from(array, dec => ('0' + dec.toString(16)).slice(-2)).join('');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async getCodeChallenge(verifier: string) {
|
|
751
|
+
const encoder = new TextEncoder();
|
|
752
|
+
const data = encoder.encode(verifier);
|
|
753
|
+
// 使用浏览器原生 Web Crypto API 进行 SHA-256 加密
|
|
754
|
+
const digest = await window.crypto.subtle.digest('SHA-256', data);
|
|
755
|
+
|
|
756
|
+
// Base64Url 编码 (注意:不是普通的 Base64)
|
|
757
|
+
return btoa(String.fromCharCode(...new Uint8Array(digest)))
|
|
758
|
+
.replace(/\+/g, '-')
|
|
759
|
+
.replace(/\//g, '_')
|
|
760
|
+
.replace(/=+$/, '');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 根据 code 换取 accessToken 和 refreshToken
|
|
764
|
+
@RequestMapping('/oauth2/token', 'POST')
|
|
765
|
+
@RequestConfig(function (source) {
|
|
766
|
+
const {signal, ...data} = source;
|
|
767
|
+
return {
|
|
768
|
+
headers: {
|
|
769
|
+
'Authorization': 'Basic ' + btoa('test-client:test-secret'),
|
|
770
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
771
|
+
},
|
|
772
|
+
signal: signal,
|
|
773
|
+
data,
|
|
774
|
+
}
|
|
775
|
+
})
|
|
776
|
+
getToken(params: {
|
|
777
|
+
code_verifier: string;
|
|
778
|
+
code: string;
|
|
779
|
+
redirect_uri: string;
|
|
780
|
+
signal?: AbortSignal
|
|
781
|
+
}) {
|
|
782
|
+
return Expect<{
|
|
783
|
+
access_token: string;
|
|
784
|
+
refresh_token: string;
|
|
785
|
+
}>({
|
|
786
|
+
signal: params.signal,
|
|
787
|
+
grant_type: 'authorization_code',
|
|
788
|
+
code: params.code,
|
|
789
|
+
client_id: 'test-client',
|
|
790
|
+
redirect_uri: params.redirect_uri,
|
|
791
|
+
code_verifier: params.code_verifier
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 刷新 accessToken
|
|
796
|
+
@RequestMapping('/oauth2/token', 'POST')
|
|
797
|
+
@RequestConfig(function (source) {
|
|
798
|
+
const {signal, ...data} = source;
|
|
799
|
+
return {
|
|
800
|
+
headers: {
|
|
801
|
+
'Authorization': 'Basic ' + btoa('test-client:test-secret'),
|
|
802
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
803
|
+
},
|
|
804
|
+
signal: signal,
|
|
805
|
+
data,
|
|
806
|
+
}
|
|
807
|
+
})
|
|
808
|
+
refreshToken(params: {
|
|
809
|
+
refresh_token: string;
|
|
810
|
+
signal?: AbortSignal
|
|
811
|
+
}) {
|
|
812
|
+
return Expect<{
|
|
813
|
+
"access_token": string;
|
|
814
|
+
"refresh_token": string;
|
|
815
|
+
"scope": string;
|
|
816
|
+
"token_type": string;
|
|
817
|
+
"expires_in": number;
|
|
818
|
+
}>({
|
|
819
|
+
signal: params.signal,
|
|
820
|
+
grant_type: 'refresh_token',
|
|
821
|
+
refresh_token: params.refresh_token,
|
|
822
|
+
client_id: 'test-client'
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// 业务接口
|
|
828
|
+
@RequestConfig(businessConfig)
|
|
829
|
+
export class BusinessService extends Service {
|
|
830
|
+
// ... 全部省略
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
+ 登录并写入授权信息
|
|
834
|
+
```ts
|
|
835
|
+
const oauthService = new OAuthService();
|
|
836
|
+
|
|
837
|
+
async function handleLoginButtonClick() {
|
|
838
|
+
const verifier = oauthService.generateVerifier();
|
|
839
|
+
sessionStorage.setItem('code_verifier', verifier);
|
|
840
|
+
const challenge = await oauthService.getCodeChallenge(verifier);
|
|
841
|
+
const params = new URLSearchParams({
|
|
842
|
+
response_type: 'code',
|
|
843
|
+
client_id: 'test-client',
|
|
844
|
+
scope: 'read',
|
|
845
|
+
redirect_uri: 'http://localhost:5173',
|
|
846
|
+
code_challenge: challenge,
|
|
847
|
+
code_challenge_method: 'S256'
|
|
848
|
+
});
|
|
849
|
+
window.location.href = `http://localhost:8080/oauth2/authorize?${params.toString()}`
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// 服务器回调的路由页面,这里直接用首页
|
|
853
|
+
useEffect(() => {
|
|
854
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
855
|
+
const code = urlParams.get('code');
|
|
856
|
+
const controller = new AbortController();
|
|
857
|
+
const verifier = sessionStorage.getItem('code_verifier');
|
|
858
|
+
|
|
859
|
+
if (code && verifier) {
|
|
860
|
+
oauthService.getToken({
|
|
861
|
+
code: code,
|
|
862
|
+
redirect_uri: 'http://localhost:5173',
|
|
863
|
+
code_verifier: verifier,
|
|
864
|
+
signal: controller.signal,
|
|
865
|
+
}).then((response) => {
|
|
866
|
+
const {access_token, refresh_token} = response.data;
|
|
867
|
+
authorizer.storageSession({access_token, refresh_token}).then(() => {
|
|
868
|
+
console.log({access_token, refresh_token});
|
|
869
|
+
});
|
|
870
|
+
// 消除地址栏的 code
|
|
871
|
+
window.history.replaceState({}, document.title, window.location.pathname);
|
|
872
|
+
}).catch(error => {
|
|
873
|
+
if (error.name !== 'AbortError') console.error("Failed to get token:", error);
|
|
874
|
+
});
|
|
875
|
+
} else {
|
|
876
|
+
authorizer.getSession().then(session => {
|
|
877
|
+
// ...
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return () => controller.abort();
|
|
881
|
+
}, []);
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
## Authorizer
|
|
885
|
+
会话对象结构:
|
|
886
|
+
```ts
|
|
887
|
+
export type BasicSession = {
|
|
888
|
+
access_token?: string;
|
|
889
|
+
accessToken?: string;
|
|
890
|
+
token?: string;
|
|
891
|
+
|
|
892
|
+
refreshToken?: string;
|
|
893
|
+
refresh_token?: string;
|
|
894
|
+
} & Record<string, any>;
|
|
895
|
+
```
|
|
896
|
+
非标准授权凭证需要映射到`access_token`/`accessToken`/`token`中的一个或多个字段。
|
|
897
|
+
|
|
898
|
+
非标准授权流程不支持`refreshToken`,可设置为不重复的随机值。
|
|
899
|
+
|
|
900
|
+
必须重载的方法只有`onAuthorizedDenied`、`refreshSession`。
|
|
901
|
+
|
|
902
|
+
**方法说明**:
|
|
903
|
+
|
|
904
|
+
+ invalidateSession(): Promise<void>
|
|
905
|
+
> 删除会话信息,并触发 `onSessionInvalidated` 回调。
|
|
906
|
+
+ onSessionInvalidated(): void
|
|
907
|
+
> 可重载该方法返回登录页或首页。
|
|
908
|
+
+ onAuthorizedDenied(error: unknown): Promise<void>
|
|
909
|
+
> 处理 `refreshSession` 运行时异常(HTTP 401/500/RuntimeError),通常就是`refreshToken`过期需要重新登录。
|
|
910
|
+
> 重载该方法执行清理工作,默认实现直接 `rethrow` 异常,你的控制台会始终报错误信息。
|
|
911
|
+
> 该方法应始终重载,务必调用 `invalidateSession` 删除会话信息。
|
|
912
|
+
+ refreshSession(session: BasicSession): Promise<BasicSession | null>
|
|
913
|
+
> 该方法应始终重载,参数是当前会话信息,根据`session.refreshToken`换取新的`accessToken`,返回新会话对象(`PlainObject`),插件会自动持久化。
|
|
914
|
+
> 如果是非标准的授权流程(不支持续期,无`refreshToken`等)直接返回`null`即可,并调用`invalidateSession`移除会话信息。或者直接抛出自定义异常走`onAuthorizedDenied`清理流程。
|
|
915
|
+
> 如果支持无感知的`refreshToken`重新获取,也在该方法进行。
|
|
916
|
+
+ checkResponse(response: AxiosResponse): boolean
|
|
917
|
+
> 检查请求是否触发了授权过期错误,默认**只检查状态码**是否为`401`,返回`false`表示授权过期。
|
|
918
|
+
> 授权过期触发续期流程,插件会先调用`getSession`获取会话对象,会话对象将用于:
|
|
919
|
+
>
|
|
920
|
+
> 1. 传给`checkSession`进行会话信息和请求的匹配,因为当前会话信息已过期,应当返回`false`表示校验不通过,返回 `true` 直接抛出原始异常,跳过续期流程。
|
|
921
|
+
> 2. `checkSession`返回`false`将调用`refreshSession`,并传入当前带有`refreshToken`的会话对象作为参数。
|
|
922
|
+
+ checkSession(request: InternalAxiosRequestConfig, session: BasicSession): boolean
|
|
923
|
+
> 默认实现为提取 `authorizer.withAuthentication` 写入请求头的`accessToken`与当前过期会话信息的`accessToken`匹配,相等返回`false`。
|
|
924
|
+
+ withAuthentication(request: InternalAxiosRequestConfig, session: BasicSession): void
|
|
925
|
+
> 拦截请求注入会话信息,默认实现为将`accessToken`写入请求头`Authorization: Bearer ${accessToken}`。
|
|
926
|
+
> 如果`refreshSession`返回`null`,那么插件正常重载的情况下`getSession`会返回`null`,会话对象为空`withAuthentication`不会执行请求头写入。
|
|
927
|
+
+ getSession(): Promise<BasicSession>
|
|
928
|
+
> 获取会话信息对象,其实就是调用`sessionStorage.get`。
|
|
929
|
+
+ storageSession(session: BasicSession | null)
|
|
930
|
+
> 调用`sessionStorage.set`。
|
|
931
|
+
|
|
932
|
+
---
|
|
933
|
+
|
|
934
|
+
```mermaid
|
|
935
|
+
graph TD
|
|
936
|
+
%% 请求拦截器部分
|
|
937
|
+
Start((开始请求)) --> ReqInterceptor[<b>请求拦截器</b>]
|
|
938
|
+
ReqInterceptor --> GetSession[authorizer.getSession 获取会话]
|
|
939
|
+
GetSession --> InjectHeader[authorizer.withAuthentication<br/>注入 Authorization 响应头]
|
|
940
|
+
InjectHeader --> SendRequest[发送 Axios 请求]
|
|
941
|
+
|
|
942
|
+
%% 响应拦截器部分
|
|
943
|
+
SendRequest --> RespInterceptor{<b>响应拦截器</b><br/>状态码判断}
|
|
944
|
+
RespInterceptor -- "2xx (成功)" --> Success((返回数据))
|
|
945
|
+
|
|
946
|
+
RespInterceptor -- "401 (授权失效)" --> CheckUnauthorized{当前是否正在<br/>处理刷新流程?}
|
|
947
|
+
|
|
948
|
+
%% 队列逻辑
|
|
949
|
+
CheckUnauthorized -- "是 (unauthorized=true)" --> QueueRequest[将请求放入 PendingQueue 队列等待]
|
|
950
|
+
QueueRequest --> WaitRefresh[等待刷新成功后自动重发]
|
|
951
|
+
|
|
952
|
+
%% 核心刷新逻辑
|
|
953
|
+
CheckUnauthorized -- "否 (unauthorized=false)" --> SetLock[设置锁 unauthorized=true]
|
|
954
|
+
SetLock --> CheckHistory{refreshToken<br/>是否已作废?}
|
|
955
|
+
|
|
956
|
+
CheckHistory -- "是 (已作废)" --> Denied[authorizer.onAuthorizedDenied<br/>跳转登录页/清理会话]
|
|
957
|
+
|
|
958
|
+
CheckHistory -- "否" --> CheckSessionMatch{checkSession 当前 Session 与<br/>请求 Session 是否一致?}
|
|
959
|
+
|
|
960
|
+
%% 避免重复刷新
|
|
961
|
+
CheckSessionMatch -- "不一致 (已被他人刷新)" --> PopQueue[直接开始消耗队列重发]
|
|
962
|
+
|
|
963
|
+
CheckSessionMatch -- "一致 (需执行刷新)" --> RefreshAction[<b>authorizer.refreshSession</b><br/>调用刷新接口]
|
|
964
|
+
|
|
965
|
+
RefreshAction -- "刷新失败 (Token过期)" --> Deprecate[标记当前 Session 作废]
|
|
966
|
+
Deprecate --> Denied
|
|
967
|
+
|
|
968
|
+
RefreshAction -- "刷新成功" --> UpdateSession[authorizer.storageSession<br/>更新持久化存储]
|
|
969
|
+
UpdateSession --> ReleaseLock[释放锁 unauthorized=false]
|
|
970
|
+
ReleaseLock --> PopQueue
|
|
971
|
+
|
|
972
|
+
PopQueue --> ResendAll[依次重发队列中的所有请求]
|
|
973
|
+
ResendAll --> Success
|
|
974
|
+
|
|
975
|
+
%% 其他错误
|
|
976
|
+
RespInterceptor -- "5xx/其他错误" --> Error((抛出异常))
|
|
977
|
+
```
|