UNPKG

@snail-js/api

Version:

Http Request with Decorators Api, build on axios

679 lines (528 loc) 16.1 kB
<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 @snail-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"; @Server({ 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"; @Api("user") class UserApi extends SnailApi { @Get() get(@Query("id") id: string) {} @Post() create(@Data() 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`(如:@Get@Post...)装饰的方法创建一个代理,返回一个函数,此函数包含请求参数,调用此函数返回`SnailMethod` 实例 ### `SnailMethod` 实例方法 - `send` 发送请求 _异步函数,发送当前请求_ - `onSuccess` 请求成功回调 _注册请求成功事件_ - `onError` 请求失败回调 _注册请求失败事件_ - `onHitCache` 请求命中缓存回调 _注册请求命中缓存事件_ - `onFinish` 请求完成回调 _注册请求完成事件_ - `registerStrategies` 注册策略 _注册方法级策略_ ### `SnailMethod` 实例属性 - response : AxiosResponse - request : AxiosRequestConfig ,最终请求的 request,这个 request 是被 VersioningStrategy 处理过的 - 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()`装饰自定义 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`共同拼接组成最终请求路径 ## 参数装饰器 ### 查询参数 `@Query` - `@Query(key?:string)` - 单个参数使用 ```typescript @Api("user") class UserApi { @Get() get(@Query("id") id: string, @Query("sign") sign: string) {} } ``` > 传入 key,标记单个查询参数,拼接到请求`?k1=v1&k2=v2` ### 路由参数 `@Params` - `@Params(key?:string)` - 单个参数使用 ```typescript @Api("user/:id/:sign") class UserApi { @Get() get(@Params("id") id: string, @Params("sign") sign: string) {} } ``` > 传入 key,标记单个查询参数,拼接到请求`?k1=v1&k2=v2` - 对象参数使用 ```typescript class RouteParams { id: string; sign: string; } @Api("user/:id/:sign") class UserApi { @Get() get(@Params() params: RouteParams) {} } ``` > 不传入 key,会被标记为对象类型查询参数;也能自动拼接到请求 - 混合使用 ```typescript class RouteParams { id: string; sign: string; } @Api("user/:id/:sign") class UserApi { @Get() get(@Params() params: Query, @Params("a") a: number) {} } ``` ### 请求数据 - `@Data(key?:string)` - 使用方式和`@Params`相同,也支持混合使用 ## 策略装饰器`@UseStrategy` - `@UseStrategy(...Strategy[])` ### 请求策略 - 在请求发送前执行,后面的策略返回结果会覆盖前面的策略 - 若返回处理后的 request,则使用处理后的 request 发送请求,否则使用原始 request 或上一个策略返回的 request 发送请求 ```typescript class CustomStrategy extends Strategy { applyRequest(request: AxiosRequestConfig) { request.headers["Access-Token"] = "abcde"; return request; } } // 用在Snail,全局的请求策略 @Server({ baseURL: "/api", timeout: 5000, }) @UseStrategy(CustomStrategy) class BackEnd extends Snail<ShanheResponse> {} export const Service = new BackEnd(); // 创建Service实例后再注册策略 Service.registerStrategies(CustomStrategy); // 用在Api, 当Api下的方法请求时生效 @Api("test") @UseStrategy(CustomStrategy) class Test {} const TestApi = Service.createApi(Test); // 创建Api实例后再注册策略 TestApi.registerStrategies(CustomStrategy); // 用在方法,此方法请求时生效 @Api("test") @UseStrategy(CustomStrategy) class Test { @Get() @UseStrategy(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; } } ``` ## 版本管理装饰器`@Versioning`和`@Version` ### 版本管理器`@Versioning(VersioningOption)` - 全局管理版本 ```typescript @Server({ baseURL: "/api", timeout: 5000, }) @Versioning({ 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; } ``` ### 临时版本修改器`@Version` - 临时改变方法请求的版本 ```typescript @Api("test") class Test { @Get("HelloWorld") @Version("0.2.0") test() {} } ``` > 临时改变 api 版本,便于测试 ### 缓存装饰器`@HitSource` - `@HitSource(name:string)` - 为被装饰的方法设置缓存失效源,当设置的名称方法被调用且正常响应时,被装饰的方法缓存失效 - name 格式为:`serverName:apiName:methodName` > 注意:若您配置了 SnailServer/SnailApi 的 name 选项,请使用此 name 作为名称,否则使用类名作为名称 ```typescript @Api("test", { name: "api1" }) @HitSource("api1") class Test { @Get("HelloWorld") @HitSource("api1.test2") test1() {} @Post() test2() {} @Get() // Test类下任何请求成功,这个方法的缓存都会失效 @HitSource("api1") test3() {} } ``` > 当请求`[Post]test`成功时,`[Get]test/HelloWorld`的缓存失效 > 默认仅 Get 方法会进行缓存 ing 缓存,若要开启其他方法的缓存,请使用`@Server({cacheFor:'all'})`配置 > `test3`方法请求成功时,不缓存 > 注意:要使用缓存,请配置`@Server({CacheManage})`缓存管理器 ### 上传进度装饰器`@UploadProgress` - `@UploadProgress((progressEvent: AxiosProgressEvent) => void)` ### 下载进度装饰器`@DownloadProgress` - `@DownloadProgress((progressEvent: AxiosProgressEvent) => void)` ## Server Send Event 服务端推送 ### 创建 sse 端点 ```typescript @Sse("sse") class ServerSend extend SnailSse { @OnSseOpen() handleOpen(event: Event) { console.log("sse-open:", event); } @OnSseError() handleError(event: Event) { console.log("sse-error:", event); } // 处理默认message事件 @SseEvent() handleEvent(event: MessageEvent) { console.log("sse-event[message]:", event.data); } // 处理自定义名称事件 @SseEvent("chunk") handleEvent(event: Event) { console.log("sse-event[chunk]:", event); } } export const Sse = Service.createSse(ServerSend); ``` ### 服务端推送装饰器`@Sse` - `@Sse(path:string,options?:{withCredentials?: boolean,version?: string;})` - 创建一个服务端推送连接,返回一个函数,用于打开 sse 连接 - 返回的打开函数调用后会返回`{eventSource:EventSource,close:function}` - eventSource: sse 连接实例 - close: 关闭此 sse 连接的方法 ### 注册`onopen`装饰器`@OnSseOpen` - 当`@Sse`装饰的方法被调用时,将`@OnSseOpen`装饰的方法注册为`@Sse`装饰的方法返回的`EventSource`实例`onopen`处理函数 ### 注册`onerror`装饰器`@OnSseError` - 当`@Sse`装饰的方法被调用时,将`@OnSseError`装饰的方法注册为`@Sse`装饰的方法返回的`EventSource`实例`onerror`处理函数 ### 事件处理装饰器`@SseEvent` - `@SseEvent(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"; @Server({ 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 @Server({ 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