引言
在现代前端开发中,与后端API的交互是不可避免的。传统的做法是手动编写HTTP请求代码,这不仅耗时且容易出错。Pullcode作为一个基于OpenAPI规范的TypeScript HTTP客户端代码生成器,通过解析Swagger 2.0和OpenAPI 3.0文档,自动生成类型安全的API客户端代码,大大提高了开发效率。
本文将深入分析Pullcode的源码架构、核心实现原理以及在实际项目中的应用,帮助开发者快速理解并应用这个工具。
项目架构概览
核心模块结构
Pullcode采用模块化设计,主要包含以下几个核心模块:
src/
├── index.ts # 主入口文件
├── generators/ # 代码生成器
│ ├── ServiceGenerator.ts # 服务类生成器
│ ├── TypesGenerator.ts # 类型定义生成器
│ └── BizServiceGenerator.ts # 业务服务基类生成器
├── models/ # 数据模型
│ ├── OpenServer.ts # OpenAPI服务器模型
│ ├── OpenService.ts # 服务模型
│ ├── OpenRoute.ts # 路由模型
│ └── OpenType.ts # 类型模型
├── httputil/ # HTTP工具类
│ ├── Axios.ts # Axios封装
│ └── axiosTransform.ts # 请求响应转换
└── types/ # 类型定义
技术栈分析
从package.json可以看出,Pullcode使用了以下核心技术:
- TypeScript: 提供完整的类型支持
- Axios: HTTP客户端库
- EJS: 模板引擎,用于代码生成
- Commander.js: 命令行工具
- swagger2openapi: Swagger 2.0转OpenAPI 3.0转换器
- Prettier: 代码格式化
核心实现原理
1. 文档解析与转换
Pullcode首先需要处理不同版本的OpenAPI文档。在src/index.ts中,我们可以看到文档处理的逻辑:
// 支持Swagger 2.0和OpenAPI 3.0
if ((obj as OpenAPIV2.Document).swagger) {
// 使用swagger2openapi转换Swagger 2.0到OpenAPI 3.0
converter.convert(obj, _options, function (err, ret) {
if (err) {
throw err
}
genCode(ret.openapi)
});
} else {
// 直接处理OpenAPI 3.0文档
genCode(obj as OpenAPIV3.Document)
}
2. OpenAPI文档模型转换
OpenServer.convert()方法是整个转换过程的核心,它将OpenAPI 3.0文档转换为内部模型:
public static convert(document: OpenAPIV3.Document): OpenServer {
const openServer = new OpenServer();
// 1. 提取服务器信息
const servers = document.servers;
if (servers && servers.length) {
openServer.name = servers[0].url
}
// 2. 解析类型定义
const types: OpenType[] = [];
const schemas = document.components && document.components.schemas;
if (schemas) {
for (let prop in schemas) {
types.push(OpenType.fromSchema({
schema: schemas[prop] as OpenAPI3Schema,
createEnum: true,
typeName: prop.replace(/[^a-zA-Z0-9_]/g, ""),
}))
}
}
// 3. 解析API路径并分组
const paths = document.paths;
const pathItemWrapperMap = new Map<string, PathItemWrapper[]>();
// 按服务名分组API路径
for (let prop in paths) {
let key = _.trimStart(prop, "/");
if (!key) continue;
const split = key.split("/");
const wrapper = new PathItemWrapper();
const pathItem = paths[prop];
const tags = OpenServer.getTags(pathItem as OpenAPIV3.PathItemObject);
// 使用第二个tag作为服务类名,否则使用路径第一部分
let service = _.upperFirst(split[0].replace(/[^a-zA-Z0-9_]/g, "")) + "Service";
if (tags && tags.length > 1 && tags[1]) {
service = _.upperFirst(tags[1].replace(/[^a-zA-Z0-9_]/g, "")) + "Service";
}
wrapper.service = service;
wrapper.endpoint = prop.replace(/{/g, "${params.");
wrapper.pathItem = pathItem;
const wrappers = pathItemWrapperMap.get(service) || [];
wrappers.push(wrapper)
pathItemWrapperMap.set(service, wrappers);
}
// 4. 生成服务对象
const services = [] as OpenService[];
pathItemWrapperMap.forEach((wrappers: PathItemWrapper[], key: string) => {
const service = new OpenService();
service.name = key;
service.module = wrappers[0].module;
const routes = [] as OpenRoute[];
for (let wrapper of wrappers) {
const pathItem = wrapper.pathItem!;
// 为每个HTTP方法创建路由
pathItem.get && routes.push(OpenRoute.of(wrapper.endpoint!, "get", pathItem.get as OpenAPI3Operation, document.components || {}))
pathItem.post && routes.push(OpenRoute.of(wrapper.endpoint!, "post", pathItem.post as OpenAPI3Operation, document.components || {}))
pathItem.put && routes.push(OpenRoute.of(wrapper.endpoint!, "put", pathItem.put as OpenAPI3Operation, document.components || {}))
pathItem.delete && routes.push(OpenRoute.of(wrapper.endpoint!, "delete", pathItem.delete as OpenAPI3Operation, document.components || {}))
}
service.routes = routes;
service.types = openServer.types?.map(t => t.name) as any
services.push(service)
})
openServer.services = services;
return openServer;
}
3. 代码生成器架构
Pullcode使用模板引擎EJS来生成代码,每个生成器都对应一个模板文件:
ServiceGenerator - 服务类生成器
export class ServiceGenerator {
public service: OpenService | undefined;
public outputDir: string = '';
private static template: string = 'Service.ts.ejs';
public generate() {
if (!this.service) {
return
}
const name = _.upperFirst(this.service.name);
let out = path.join(this.outputDir, name + ".ts")
// 使用EJS模板渲染代码
ejs.renderFile(path.resolve(__dirname, ServiceGenerator.template), {
version,
types: this.service.types,
name,
module,
routes: this.service.routes,
_,
}, {}, function (err, str) {
if (err) {
throw err;
}
// 使用Prettier格式化生成的代码
fs.writeFile(out, prettier.format(str, { parser: "typescript" }), function (err) {
if (err) {
throw err;
}
console.log(`file ${out} is generated`)
})
});
}
}
生成的Service类模板(Service.ts.ejs)包含:
export class <%- name %> extends BizService {
constructor(options?: Partial<CreateAxiosOptions>) {
super(options);
}
<%_ routes.forEach(function(route){ -%>
/**
* <%- _.toUpper(route.httpMethod) %> <%- route.endpoint %>
* @param <%- route.reqBody.name %> <%- route.reqBody.doc %>
* @returns Promise<<%- route.respData.type %>>
*/
<%- route.name %>(<%- route.reqBody.name %>: <%- route.reqBody.type %>): Promise<<%- route.respData.type %>> {
return this.getAxios().<%- route.httpMethod %>(`<%- route.endpoint %>`, <%- route.reqBody.name %>, {});
}
<%_ }) -%>
}
TypesGenerator - 类型定义生成器
export class TypesGenerator {
public types: OpenType[] | undefined;
public outputDir: string = '';
private static template: string = 'types.ts.ejs';
public generate() {
if (!this.types || !this.types.length) {
return
}
ejs.renderFile(path.resolve(__dirname, TypesGenerator.template), {
version,
types: this.types,
}, {}, function (err, str) {
if (err) {
throw err;
}
fs.writeFile(out, prettier.format(str, { parser: "typescript" }), function (err) {
if (err) {
throw err;
}
console.log(`file ${out} is generated`)
})
});
}
}
4. HTTP客户端封装
Pullcode基于Axios构建了一个功能强大的HTTP客户端封装:
VAxios类 - 核心HTTP客户端
export class VAxios {
private axiosInstance: AxiosInstance;
private readonly options: CreateAxiosOptions;
constructor(options?: Partial<CreateAxiosOptions>) {
this.options = merge(cloneDeep(defaultOptions), options || {});
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
/**
* 设置请求和响应拦截器
*/
private setupInterceptors() {
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = this.getTransform() || {};
// 请求拦截器
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
const { requestOptions, axiosCanceler, tokenGetter } = this.options;
if (requestOptions && isObject(requestOptions) && Object.keys(requestOptions).length) {
const { apiUrl, joinPrefix, formatDate, joinTime = true, urlPrefix } = requestOptions;
// URL前缀处理
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
// 参数格式化
const params = config.params || {};
const data = config.data || false;
formatDate && data && !isString(data) && formatRequestDate(data);
// GET请求添加时间戳
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) {
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
}
}
// Token处理
if (tokenGetter && isFunction(tokenGetter)) {
const token = tokenGetter();
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
(config as Recordable).headers.Authorization = this.options.authenticationScheme
? `${this.options.authenticationScheme} ${token}`
: token;
}
}
// 自定义请求拦截器
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// 响应拦截器
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
const { axiosCanceler } = this.options;
res && axiosCanceler && axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
if (this.options.requestOptions && this.options.requestOptions.isReturnNativeResponse) {
return responseInterceptors(res, this.options)
}
return responseInterceptors(res.data, this.options)
}
if (this.options.requestOptions && this.options.requestOptions.isReturnNativeResponse) {
return res
}
return res.data
}, undefined);
}
}
实际应用示例
1. 快速开始
安装依赖
npm install --save pullcode
配置npm script
{
"scripts": {
"pull": "pullcode -u https://petstore3.swagger.io/api/v3/openapi.json -o src/api"
}
}
生成代码
npm run pull
2. 生成的代码结构
执行命令后,会在src/api目录下生成以下文件:
src/api/
├── BizService.ts # 业务服务基类(可编辑)
├── PetService.ts # 宠物服务类
├── StoreService.ts # 商店服务类
├── UserService.ts # 用户服务类
└── types.ts # 类型定义
BizService.ts - 配置基础服务
import { merge } from "lodash-es";
import { CreateAxiosOptions, VAxios } from "pullcode/dist/esm/httputil/Axios";
import { RequestOptions } from "pullcode/dist/esm/types/axios";
const defaultOptions: CreateAxiosOptions = {
requestOptions: {
apiUrl: 'https://petstore3.swagger.io/api/v3', // API基础URL
urlPrefix: '', // URL前缀
} as RequestOptions,
};
export class BizService extends VAxios {
constructor(options?: Partial<CreateAxiosOptions>) {
super(merge(defaultOptions, options || {}));
}
}
PetService.ts - 生成的API客户端
export class PetService extends BizService {
constructor(options?: Partial<CreateAxiosOptions>) {
super(options);
}
/**
* GET /pet/findByStatus
* Multiple status values can be provided with comma separated strings
* @param params
* @returns Promise<Pet[]>
*/
getPetFindByStatus(params: {
status?: PetStatusEnum;
}): Promise<Pet[]> {
return this.getAxios().get(`/pet/findByStatus`, {
params: qs.stringify(params),
});
}
/**
* POST /pet
* Add a new pet to the store.
* @param payload Create a new pet in the store
* @returns Promise<Pet>
*/
postPet(payload: Pet): Promise<Pet> {
return this.getAxios().post(`/pet`, payload, {});
}
}
types.ts - 类型定义
export interface Pet {
id?: number;
name: string;
category?: Category;
photoUrls: string[];
tags?: Tag[];
status?: PetStatusEnum;
}
export enum PetStatusEnum {
AVAILABLE = 'available',
PENDING = 'pending',
SOLD = 'sold',
}
3. 在Vue项目中使用
<script setup lang="ts">
import { petService } from "@/api/PetService";
import { Pet, PetStatusEnum } from "@/api/types";
import { ref } from "vue";
const loading = ref(true);
const dataSource = ref([] as Pet[]);
// 调用API
petService
.getPetFindByStatus({
status: PetStatusEnum.AVAILABLE,
})
.then((resp: Pet[]) => {
dataSource.value = resp;
loading.value = false;
})
.catch((error) => {
console.error('获取宠物列表失败:', error);
loading.value = false;
});
</script>
<template>
<div>
<a-table :dataSource="dataSource" :columns="columns" :loading="loading">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'tags'">
<span>
<a-tag v-for="tag in record.tags" :key="tag.id">
{{ tag.name.toUpperCase() }}
</a-tag>
</span>
</template>
<template v-else-if="column.key === 'category'">
{{ record.category?.name }}
</template>
</template>
</a-table>
</div>
</template>
4. 高级配置
自定义拦截器
// src/api/interceptor.ts
import type { AxiosResponse } from 'axios';
import type { AxiosTransform } from 'pullcode/dist/esm/httputil/axiosTransform';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
export const transform: AxiosTransform = {
/**
* 响应错误处理
*/
responseInterceptorsCatch: (_: AxiosResponse, error: any) => {
const { response, code, message } = error || {};
let errMessage = '';
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
errMessage = '请求超时';
}
if (error?.includes('Network Error')) {
errMessage = '网络异常';
}
if (errMessage) {
createMessage.error(errMessage);
return Promise.reject(error);
}
return Promise.reject(error);
},
};
配置Token获取
// src/api/BizService.ts
import { getToken } from '/@/utils/auth';
const defaultOptions: CreateAxiosOptions = {
transform,
requestOptions: {
apiUrl: globSetting.apiUrl,
urlPrefix: '/myservice',
} as RequestOptions,
tokenGetter: getToken as () => string
}
技术优势分析
1. 类型安全
Pullcode生成的代码具有完整的TypeScript类型支持:
- 接口参数类型: 根据OpenAPI schema自动生成
- 响应数据类型: 精确的类型定义
- 枚举类型: 自动生成枚举值
- 可选参数: 正确处理required字段
2. 开发体验
- 智能提示: IDE提供完整的代码补全
- 错误检查: 编译时发现类型错误
- 文档注释: 自动生成JSDoc注释
- 代码格式化: 使用Prettier保持代码风格一致
3. 可扩展性
- 拦截器支持: 可自定义请求和响应拦截器
- 配置灵活: 支持全局和局部配置
- 错误处理: 统一的错误处理机制
- Token管理: 自动的认证token处理
4. 框架无关
Pullcode生成的代码是纯TypeScript,不依赖特定框架,可以在任何支持TypeScript的项目中使用:
- Vue 2/3
- React
- Angular
- Node.js
- 其他TypeScript项目
项目配置说明
TypeScript配置
确保tsconfig.json包含以下配置:
{
"compilerOptions": {
"importsNotUsedAsValues": "remove"
}
}
说明:importsNotUsedAsValues: "remove" 配置允许直接导入类型而不需要 type 关键字,这是 pullcode 生成的代码所必需的。
推荐目录结构
src/
├── api/ # 生成的API代码
│ ├── BizService.ts # 基础配置
│ ├── PetService.ts # 宠物API
│ ├── UserService.ts # 用户API
│ └── types.ts # 类型定义
├── utils/
│ └── auth.ts # 认证工具
└── components/
└── PetList.vue # 使用API的组件
总结
Pullcode作为一个基于OpenAPI规范的TypeScript HTTP客户端代码生成器,通过以下技术特点为前端开发带来了显著的价值:
- 自动化代码生成: 从OpenAPI文档自动生成类型安全的API客户端
- 完整的类型支持: 基于TypeScript提供完整的类型定义
- 灵活的配置: 支持自定义拦截器、错误处理等
- 框架无关: 可在任何TypeScript项目中使用
- 开发体验优化: 提供智能提示、错误检查等功能
通过深入理解Pullcode的源码架构和实现原理,开发者可以更好地利用这个工具,提高API开发的效率和质量。在实际项目中,合理配置和使用Pullcode可以大大减少手动编写API客户端代码的工作量,同时确保类型安全和代码质量。