@snail-js/api
Version:
Http Request with Decorators Api, build on axios
679 lines (528 loc) • 16.1 kB
Markdown
<p>
<img src="https://img.shields.io/badge/TypeScript-1e80ff"></img>
<img src="https://img.shields.io/npm/v/axios?label=axios&labelColor=1e80ff&color=67C23A"></img>
<img src="https://img.shields.io/npm/v/reflect-metadata?label=reflect-metadata&labelColor=1e80ff&color=67C23A"></img>
</p>
中文文档|<a href='https://github.com/limingchang/snail/blob/master/packages/api/README_EN.md'>English Document</a>
## 项目介绍
- 基于 Axios 二次封装
- 使用`reflect-metadata`创建和处理元数据
- 提供装饰器定义请求请求的方式,支持所有请求方法和 SSE
## 安装
`npm install -js/api`
## 使用
1. 请开启`TypeScript`相关装饰器配置
```json
// tsconfig.json
{
"module": "ESNext",
// 模块解析策略
"moduleResolution": "node",
"baseUrl": ".",
// target 必须大于ES6
"target": "ESNext",
// lib 需要包含大于ES6的ES版本
"lib": ["ESNext", "DOM"],
// 包含reflect-metadata类型
"types": ["reflect-metadata"],
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"strictNullChecks": false
}
```
2. 创建`Snail`后端基本配置实例
```typescript
// service.ts
import { SnailServer, Server } from "@snail-js/api";
({
baseURL: "/api",
timeout: 5000,
})
class BackEnd extends SnailServer {}
export const Service = new BackEnd();
```
3. 创建 Api 实例
```typescript
// user.ts
import { Api, Get, Post, Query, Data, SnailApi } from "@snail-js/api";
import { Service } from "./service";
("user")
class UserApi extends SnailApi {
()
get(("id") id: string) {}
()
create(() user: User) {}
}
// 创建并导出api
export const userApi = Service.createApi(UserApi);
```
3. 发送请求
```typescript
import { userApi } from "./user";
const { send:getUser, onSuccess, onError, onHitCache } = await userApi.get();
const data = await getUser("1");
```
## `SnailMethod` 实例
- 调用`Service.createApi(ApiInstance)`后会为`ApiInstance`内被`RequestMethod`(如:、...)装饰的方法创建一个代理,返回一个函数,此函数包含请求参数,调用此函数返回`SnailMethod` 实例
### `SnailMethod` 实例方法
- `send` 发送请求
_异步函数,发送当前请求_
- `onSuccess` 请求成功回调
_注册请求成功事件_
- `onError` 请求失败回调
_注册请求失败事件_
- `onHitCache` 请求命中缓存回调
_注册请求命中缓存事件_
- `onFinish` 请求完成回调
_注册请求完成事件_
- `registerStrategies` 注册策略
_注册方法级策略_
### `SnailMethod` 实例属性
- response : AxiosResponse
- request : AxiosRequestConfig ,最终请求的 request,这个 request 是被 Versioning 和 Strategy 处理过的
- version : string,最终请求的版本,如果没有开启 Versioning 则为 undefine
- name : string,完整的 SnailMethod 名称,格式为`ServerName.ApiName.MethodName`
- error : Error | null,请求失败的错误信息,无错误为 null
### Server 配置
<table>
<tr>
<th>配置项</th>
<th>类型</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
<tr>
<td>name</td>
<td>string</td>
<td>否</td>
<td>默认使用继承`SnailServer`的类名作为name</td>
<td>server实例唯一标识,请勿与其他server重复</td>
</tr>
<tr>
<td>baseUrl</td>
<td>string</td>
<td>否</td>
<td>'\'</td>
<td>请求后端的api地址前缀,同`axios`的baseUrl</td>
</tr>
<tr>
<td>Versioning</td>
<td><a href="#versioningoption">VersioningOption</a></td>
<td>否</td>
<td>undefine</td>
<td>版本管理器配置,默认不开启</td>
</tr>
<tr>
<td>timeout</td>
<td>number</td>
<td>否</td>
<td>5000</td>
<td>单位:毫秒;全局超时时间,会被 Api 的 timeout 值覆盖</td>
</tr>
<tr>
<td>cacheManage</td>
<td>{type:CacheType,ttl:number}</td>
<td>否</td>
<td>{
type: CacheType.Memory,
ttl: 500
}</td>
<td>缓存管理器,ttl单位为秒</td>
</tr>
<tr>
<td>cacheFor</td>
<td>RequestMethod | RequestMethod[] | 'All' | 'all' </td>
<td>否</td>
<td>Get</td>
<td>要启用缓存的方法,默认仅开启Get缓存</td>
</tr>
<tr>
<td>serverStatusCodeRule</td>
<td><a href="#SnailServerStatusCodeRuleOptions">SnailServerStatusCodeRuleOptions</a></td>
<td>否</td>
<td>undefined</td>
<td>服务端状态码规则,若配置此项,rule函数返回false时会触发错误</td>
</tr>
<tr>
<td>enableLog</td>
<td>boolean</td>
<td>否</td>
<td>false</td>
<td>是否开启日志,用于调试</td>
</tr>
</table>
### Api 配置
- 请使用`()`装饰自定义 Api 类并继承`SnailApi`
<table>
<tr>
<th>配置项</th>
<th>类型</th>
<th>是否必须</th>
<th>默认值</th>
<th>说明</th>
</tr>
<tr>
<td>name</td>
<td>string</td>
<td>否</td>
<td>默认使用继承`SnailApi`的类名作为name</td>
<td>api实例唯一标识,请勿与其他api重复</td>
</tr>
<tr>
<td>timeout</td>
<td>number</td>
<td>否</td>
<td></td>
<td>请求超时时间;会覆盖Server的timeout设置</td>
</tr>
<tr>
<td>version</td>
<td>string</td>
<td>否</td>
<td></td>
<td>api版本号,会覆盖server的`defaultVersion`配置</td>
</tr>
</table>
## 请求方法装饰器
- 在`Api`类中使用,用于标记请求方法
- 提供 axios 的全部请求方法`Get,Post,Head,Put,Delete,Patch,Options`
- 参数: `path?: string`; 请求端点路径,与`baseUrl,api.url`共同拼接组成最终请求路径
## 参数装饰器
### 查询参数 ``
- `(key?:string)`
- 单个参数使用
```typescript
("user")
class UserApi {
()
get(("id") id: string, ("sign") sign: string) {}
}
```
> 传入 key,标记单个查询参数,拼接到请求`?k1=v1&k2=v2`
### 路由参数 ``
- `(key?:string)`
- 单个参数使用
```typescript
("user/:id/:sign")
class UserApi {
()
get(("id") id: string, ("sign") sign: string) {}
}
```
> 传入 key,标记单个查询参数,拼接到请求`?k1=v1&k2=v2`
- 对象参数使用
```typescript
class RouteParams {
id: string;
sign: string;
}
("user/:id/:sign")
class UserApi {
()
get(() params: RouteParams) {}
}
```
> 不传入 key,会被标记为对象类型查询参数;也能自动拼接到请求
- 混合使用
```typescript
class RouteParams {
id: string;
sign: string;
}
("user/:id/:sign")
class UserApi {
()
get(() params: Query, ("a") a: number) {}
}
```
### 请求数据
- `(key?:string)`
- 使用方式和``相同,也支持混合使用
## 策略装饰器``
- `(...Strategy[])`
### 请求策略
- 在请求发送前执行,后面的策略返回结果会覆盖前面的策略
- 若返回处理后的 request,则使用处理后的 request 发送请求,否则使用原始 request 或上一个策略返回的 request 发送请求
```typescript
class CustomStrategy extends Strategy {
applyRequest(request: AxiosRequestConfig) {
request.headers["Access-Token"] = "abcde";
return request;
}
}
// 用在Snail,全局的请求策略
({
baseURL: "/api",
timeout: 5000,
})
(CustomStrategy)
class BackEnd extends Snail<ShanheResponse> {}
export const Service = new BackEnd();
// 创建Service实例后再注册策略
Service.registerStrategies(CustomStrategy);
// 用在Api, 当Api下的方法请求时生效
("test")
(CustomStrategy)
class Test {}
const TestApi = Service.createApi(Test);
// 创建Api实例后再注册策略
TestApi.registerStrategies(CustomStrategy);
// 用在方法,此方法请求时生效
("test")
(CustomStrategy)
class Test {
()
(CustomStrategy)
get() {}
}
// 发送请求前注册策略
const TestApi = Service.createApi(Test);
const getSomething = TestApi.get();
{ send, registerStrategies } = getSomething;
getSomething.registerStrategies(CustomStrategy);
```
### 响应策略
- 在收到服务器响应后执行
- 若返回处理后的 response,则使用处理后的 response 进行下一个策略或返回,否则使用原始 response 或上一个策略返回的 response 返回
```typescript
// 如何定义
class CustomStrategy extends Strategy {
applyResponse(response: AxiosResponse) {
const { status } = response;
if (status == 200) {
// do something
}
return response;
}
}
```
## 版本管理装饰器``和``
### 版本管理器`(VersioningOption)`
- 全局管理版本
```typescript
({
baseURL: "/api",
timeout: 5000,
})
({
type: VersioningType.Header,
defaultVersion: "0.1.0",
})
class BackEnd extends Snail<ShanheResponse> {}
export const Service = new BackEnd();
```
#### <a id="versioningoption">`VersioningOption`</a>类型
```typescript
export enum VersioningType {
Uri,
Header,
Query,
Custom,
}
interface VersioningCommonOption {
defaultVersion: string;
}
export interface VersioningUriOption extends VersioningCommonOption {
type: VersioningType.Uri;
prefix?: string;
}
export interface VersioningHeaderOption extends VersioningCommonOption {
type: VersioningType.Header;
header?: string;
}
export interface VersioningQueryOption extends VersioningCommonOption {
type: VersioningType.Query;
key?: string;
}
export interface VersioningCustomOption extends VersioningCommonOption {
type: VersioningType.Custom;
extractor: (requestOptions: unknown) => {
url: string;
headers: Record<string, any>;
};
}
export type VersioningOption =
| VersioningUriOption
| VersioningHeaderOption
| VersioningQueryOption
| VersioningCustomOption;
```
### <a id="SnailServerStatusCodeRuleOptions">SnailServerStatusCodeRuleOptions</a> 类型
```typescript
export class SnailServerStatusCodeRuleOptions {
// 通常服务端会返回一个状态码,当状态码不符合预期时,会抛出错误
// 可以使用此选项来定义服务端状态码的规则
// 当rule函数返回false时,会触发错误
rule: (statusCode: number) => boolean;
// response.data中服务端状态码的key,默认为code
key?: string;
}
```
### 临时版本修改器``
- 临时改变方法请求的版本
```typescript
("test")
class Test {
("HelloWorld")
("0.2.0")
test() {}
}
```
> 临时改变 api 版本,便于测试
### 缓存装饰器``
- `(name:string)`
- 为被装饰的方法设置缓存失效源,当设置的名称方法被调用且正常响应时,被装饰的方法缓存失效
- name 格式为:`serverName:apiName:methodName`
> 注意:若您配置了 SnailServer/SnailApi 的 name 选项,请使用此 name 作为名称,否则使用类名作为名称
```typescript
("test", { name: "api1" })
("api1")
class Test {
("HelloWorld")
("api1.test2")
test1() {}
()
test2() {}
()
// Test类下任何请求成功,这个方法的缓存都会失效
("api1")
test3() {}
}
```
> 当请求`[Post]test`成功时,`[Get]test/HelloWorld`的缓存失效
> 默认仅 Get 方法会进行缓存 ing 缓存,若要开启其他方法的缓存,请使用`({cacheFor:'all'})`配置
> `test3`方法请求成功时,不缓存
> 注意:要使用缓存,请配置`({CacheManage})`缓存管理器
### 上传进度装饰器``
- `((progressEvent: AxiosProgressEvent) => void)`
### 下载进度装饰器``
- `((progressEvent: AxiosProgressEvent) => void)`
## Server Send Event 服务端推送
### 创建 sse 端点
```typescript
("sse")
class ServerSend extend SnailSse {
()
handleOpen(event: Event) {
console.log("sse-open:", event);
}
()
handleError(event: Event) {
console.log("sse-error:", event);
}
// 处理默认message事件
()
handleEvent(event: MessageEvent) {
console.log("sse-event[message]:", event.data);
}
// 处理自定义名称事件
("chunk")
handleEvent(event: Event) {
console.log("sse-event[chunk]:", event);
}
}
export const Sse = Service.createSse(ServerSend);
```
### 服务端推送装饰器``
- `(path:string,options?:{withCredentials?: boolean,version?: string;})`
- 创建一个服务端推送连接,返回一个函数,用于打开 sse 连接
- 返回的打开函数调用后会返回`{eventSource:EventSource,close:function}`
- eventSource: sse 连接实例
- close: 关闭此 sse 连接的方法
### 注册`onopen`装饰器``
- 当``装饰的方法被调用时,将``装饰的方法注册为``装饰的方法返回的`EventSource`实例`onopen`处理函数
### 注册`onerror`装饰器``
- 当``装饰的方法被调用时,将``装饰的方法注册为``装饰的方法返回的`EventSource`实例`onerror`处理函数
### 事件处理装饰器``
- `(eventName?:string)`
- 未传入`eventName`,默认注册为`message`事件处理器
- 传入`eventName`,注册为对应名称的事件处理器
---
## TypeScript 支持
### 默认返回类型
```typescript
export type StandardResponseData<
T extends ResponseJsonData = Record<string, any>
> = {
code: number;
message: string;
data: T;
};
```
### 定义返回类型
1. 定义后端标准数据格式
```typescript
export class CustomResponse {
status_code: number;
msg: string;
}
```
2. 创建时应用格式
```typescript
// service.ts
import { SnailServer, Server } from "@snail-js/api";
({
baseURL: "/api",
timeout: 5000,
})
class BackEnd extends SnailServer<CustomResponse> {}
export const Service = new BackEnd();
```
3. 调用 API 时携带数据格式
```typescript
import { userApi } from "./user";
class User {
id: number;
name: string;
tel: string;
age: number;
}
const getUser = userApi.get<User>();
const { send } = getUser;
const res = await send("1");
// 默认情况,以data为key存储数据
// res.data => CustomResponse & { data : User}
```
> API 被调用的返回格式
```typescript
const getUser = userApi.get<User>();
const { send } = getUser;
const res = await send("1");
// res.data => CustomResponse & { data: User }
const getUser = userApi.get<Blob>();
const { send } = getUser;
const res = await send("1");
// res => AxiosResponse<Blob>
```
> `data: T`,后端响应数据;默认为`ResponseData<T = any>`类型;可由用户自定义修改
> `error: null | Error` ; 请求过程中的错误包含在此,可先判断 error 是否为 null 再进行数据处理
> `hitCache?: boolean`; 标识请求是否击中缓存;若从缓存获得数据,则不会发送请求。
4. 若后端返回数据不是以 data 为 key 包含数据
```typescript
({
baseURL: "/api",
timeout: 5000,
})
class BackEnd extends Snail<CustomResponse, "records"> {}
const res = await userApi.get<User>();
// 自定义数据key
// res.data => CustomResponse & { records : User}
```
### 非 json 数据的返回
- 若后端返回的 content-type 不是 json 类型,send 方法返回的将是`AxiosResponse`
- 若后端返回的 content-type 是 json 类型,send 方法返回的将是`AxiosResponse.data`
### 代码仓库
<p>
<a href="https://gitee.com/limich/snail">
<img src="https://img.shields.io/badge/snail-js?style=flat&label=gitee&labelColor=F56C6C&link=https%3A%2F%2Fgitee.com%2Flimich%2Fsnail"></img>
</a>
</p>
<p>
<a href="https://github.com/limingchang/snail">
<img src="https://img.shields.io/badge/snail-js?style=flat&label=github&labelColor=F56C6C&link=https%3A%2F%2Fgihub.com%2Flimingchang%2Fsnail"></img>
</a>
</p>
### 作者
- mc.lee