API 一体化调用
API 声明
目前 bwcx 提供了 API 声明组件,通过装饰器形式声明 API 的属性,用于前后端一体化开发接口调用。
安装依赖:npm i -S bwcx-api
使用 @Api
装饰器为路由方法声明 API 相关属性,相关路由方法必须使用 @Contract()
声明请求/响应 DTO,否则无法用于后续的接口生成:
import { Inject, Controller, Data, Get, Contract } from 'bwcx-ljsm';
import {
GetUsersReqDTO,
GetUsersRespDTO,
} from 'your-common/modules/user/user.dto';
import { Api } from 'bwcx-api';
@Controller('/user')
export default class UserController {
@Api.Summary('获取用户列表')
@Api.Description('通过查询参数获取用户列表数据')
@Get('/get')
@Contract(GetUsersReqDTO, GetUsersRespDTO)
async getUsers(@Data() data: GetUsersReqDTO): Promise<GetUsersRespDTO> {
console.log('data', data);
return { rows: [] };
}
}
配置客户端调用
bwcx 提供了 api-client
,为前后端一体化开发提供近乎无感的客户端调用代码生成。借助这个能力,你可以做到写完服务端接口,保存,即可在前端通过 api.method
像调用本地函数一般直接调用服务端接口,而无需手动处理复杂的 url 拼接、参数填充、数据类型转换等问题。
配置代码生成
安装依赖:npm i -S bwcx-api-client
在服务端 App 上简单修改,加入代码生成逻辑:
import { App, getDependency } from 'bwcx-ljsm';
import BWCX_CONTAINER_KEY from 'bwcx/lib/container-key';
import { ApiClientGenerator } from 'bwcx-api-client/generator';
class OurApp extends App {
// ...
afterStart() {
if (process.env.NODE_ENV === 'development') {
const apiClientGenerator = new ApiClientGenerator(
{
/** 生成路径 */
outFilePath: path.join(this.baseDir, './common/api/api-client.ts'),
/** 是否在文件头附加额外的自定义导入语句 */
prependImports: [],
/** 是否开启请求时额外参数,用于调用时传递额外的参数,可以改变 client 的行为 */
enableExtraReqOptions: true,
},
getDependency<IAppWiredData>(BWCX_CONTAINER_KEY.WiredData, this.container).router,
);
await apiClientGenerator.generate();
}
}
}
每次你的应用在开发时重启,都会自动生成一次 api-client,生成文件简化示例如下:
import { GetUsersReqDTO, GetUsersRespDTO } from 'your-common/modules/user/user.dto';
export class ApiClient<T = undefined> {
/**
* 获取用户列表
*
* @description 通过查询参数获取用户列表数据
* @param {GetEnterpriseLogoMainColorReqDTO} req The request data (compatible with ReqDTO).
* @param {T} opts Extra request options.
* @returns {GetEnterpriseLogoMainColorRespDTO} The response data (RespDTO).
*/
public getUsers(req: GetEnterpriseLogoMainColorReqDTO, opts?: T): Promise<GetEnterpriseLogoMainColorRespDTO> {}
}
api-client 是自动生成的接口调用代码,其使用最朴素的 HTTP 调用,内部自动根据 DTO 定义完成拼接参数、处理响应等步骤,对外暴露为和 Controller 路由方法几乎一样的函数调用。
客户端调用
确保安装了运行时所需依赖:npm i -S urlcat-fork
有了 api-client,各类客户端(Web 前端、小程序等)都可以直接调用服务端接口。为了保证各端调用的灵活性和兼容性,api-client 不会内置实现请求代码,也不依赖任何请求库,不过你只需要简单的适配即可封装出用于在客户端上使用的 client,你可以自己处理细节,诸如使用什么请求库、是否有自带参数、是否传递 csrf token 等行为。
还记得之前的响应处理器吗?响应处理器让我们只需要考虑纯粹的数据响应,在外层为响应统一做包装。同样地,客户端接收到请求,要把请求体解析回原始的响应返回(RespDTO),这就需要告诉 client 如何解析响应。因此首先需要定义一个响应解析器:
import { AbstractResponseParser } from 'bwcx-api-client';
import { ApiRequestException } from './api-request.exception'; // 可以定义一个异常给客户端使用
export class ResponseParser extends AbstractResponseParser {
public constructor() {
super({});
}
public parse(resp: any) {
// 解析响应,等同于接口的响应处理器的反向操作
if (resp.success) {
return resp.data;
}
throw new ApiRequestException(resp.code, resp.msg);
}
}
生成的 ApiClient 被调用时,会将已经自动处理过的用户请求参数传递给请求适配器函数,已处理参数的数据结构如下所示:
{
method: AllowedRequestMethod;
url: string;
/** 请求体,如果是含有文件上传的请求,会返回 FormData 对象 */
data: any;
/** 请求头 */
headers?: Record<string, string>;
/** 额外请求选项,用户调用 API 时会通过第二个参数传递 */
extraOpts?: T;
/** API 元数据 */
metadata: APIMetadata;
}
现在,只需要告诉 ApiClient 我们的自定义请求适配器是什么,它就可以运行了!请求适配器是一个函数,接收刚才提到的参数,并根据参数发起实际 HTTP 请求。以 axios 库为例:
import Axios from 'axios';
import { omit } from 'lodash';
import { IBwcxApiRequestAdaptorArgs } from 'bwcx-api-client';
import { ApiClient } from 'path-to-your-original-api-client';
import { ResponseParser } from 'path-to-your-response-parser';
// 这里可以为 client 添加自定义的请求级选项,在调用 API 时作为第二个参数传递
export interface IRequestExtraOpts {
showTips?: boolean;
}
/** 实现一个请求适配器,接收 ApiClient 提供的参数,并实现发起请求 */
export class RequestAdaptor {
public request(opts: IBwcxApiRequestAdaptorArgs<IRequestExtraOpts>) {
const { extraOpts = {}, metadata } = opts;
const config = omit(opts, 'metadata', 'extraOpts');
return Axios.request(config)
.then((response) => {
// 自定义选项的逻辑
if (!response.data.success && extraOpts.showTips) {
tips.err(response.data.msg || '服务异常,请稍候再试');
}
// 返回 HTTP 请求的响应数据,之后会交由 `ResponseParser` 处理
return response.data;
})
.catch((err) => {
if (Axios.isCancel(err)) {
return console.log('request canceled');
}
tips.err('服务异常,请稍候再试');
throw err;
});
}
}
// 拿到可用的 client 对象
export const apiClient = new ApiClient(new RequestAdaptor(), new ResponseParser());
最后,enjoy it:
import { apiClient } from 'path-to-your-client';
// 直接在客户端上调用,或挂载到组件
const res = await apiClient.getUsers({
/** 请求参数 */
});