Vue 应用与 OpenAPI/Swagger 集成:代码生成与类型安全
大家好,今天我们来探讨 Vue 应用如何与 OpenAPI/Swagger 文档集成,实现代码自动生成和类型安全。在现代前端开发中,与后端 API 的交互是核心环节。手动维护 API 调用代码不仅繁琐,而且容易出错,尤其是在 API 接口频繁变更的情况下。通过集成 OpenAPI/Swagger 文档,我们可以自动化生成 API 客户端代码,并获得类型提示,从而提高开发效率和代码质量。
1. OpenAPI/Swagger 简介
OpenAPI(前身为 Swagger)是一种用于描述 RESTful API 的标准规范。它使用 JSON 或 YAML 格式定义 API 的 endpoints、请求参数、响应结构、认证方式等信息。Swagger 是一套围绕 OpenAPI 规范的工具集,包括 Swagger Editor、Swagger UI 和 Swagger Codegen 等。
- OpenAPI 规范: 定义了 API 的描述格式。
- Swagger Editor: 用于编辑和验证 OpenAPI 文档。
- Swagger UI: 用于可视化 API 文档,方便开发者浏览和测试 API。
- Swagger Codegen (现 OpenAPI Generator): 根据 OpenAPI 文档生成各种编程语言的 API 客户端代码。
2. 集成方案选择
有多种方式可以将 OpenAPI/Swagger 集成到 Vue 应用中。这里介绍两种常用的方案:
- OpenAPI Generator (Swagger Codegen): 使用 OpenAPI Generator 根据 OpenAPI 文档直接生成 Vue 组件或 JavaScript/TypeScript 客户端代码。
- npm 包 + 自定义脚本: 使用现有的 npm 包(如
axios、openapi-typescript)结合自定义脚本,从 OpenAPI 文档生成 TypeScript 类型定义和 API 调用函数。
我们将重点介绍第二种方案,因为它更灵活,可以更好地控制代码生成过程,并且更易于集成到现有的 Vue 项目中。
3. 环境搭建与依赖安装
首先,创建一个 Vue 项目(如果还没有的话):
vue create my-vue-openapi-app
选择你喜欢的预设,例如 Vue 3 + TypeScript。
然后,安装必要的 npm 包:
npm install axios openapi-typescript
npm install -D typescript @types/node
axios: 用于发送 HTTP 请求。openapi-typescript: 用于从 OpenAPI 文档生成 TypeScript 类型定义。typescript: TypeScript 编译器。@types/node: Node.js 的 TypeScript 类型定义。
4. 获取 OpenAPI/Swagger 文档
你需要从后端获取 OpenAPI/Swagger 文档。通常,后端会提供一个 URL,例如 http://localhost:8080/api/v1/swagger.json 或 http://localhost:8080/api/v1/openapi.yaml。将文档保存到你的 Vue 项目中,例如 src/api/openapi.json。
5. 生成 TypeScript 类型定义
创建一个脚本文件,用于从 OpenAPI 文档生成 TypeScript 类型定义,例如 scripts/generate-api-types.js:
const { generate } = require('openapi-typescript');
const fs = require('fs').promises;
const path = require('path');
async function generateApiTypes() {
const openApiFilePath = path.resolve(__dirname, '../src/api/openapi.json');
const outputFilePath = path.resolve(__dirname, '../src/api/types.ts');
try {
const schema = await fs.readFile(openApiFilePath, 'utf-8');
const types = await generate(schema);
await fs.writeFile(outputFilePath, types);
console.log(`Successfully generated API types at ${outputFilePath}`);
} catch (error) {
console.error('Failed to generate API types:', error);
}
}
generateApiTypes();
这个脚本读取 src/api/openapi.json 文件,使用 openapi-typescript 生成 TypeScript 类型定义,并将结果保存到 src/api/types.ts 文件。
在 package.json 中添加一个 npm 脚本来运行这个脚本:
{
"scripts": {
"generate-api-types": "node scripts/generate-api-types.js"
}
}
现在,你可以运行 npm run generate-api-types 来生成类型定义。
6. 创建 API 客户端
创建一个 API 客户端模块,用于封装 API 调用函数,例如 src/api/client.ts:
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { paths, components } from './types'; // 导入生成的类型定义
type RequestMethod = 'get' | 'post' | 'put' | 'delete' | 'patch';
interface ApiClientOptions {
baseURL: string;
// Add other options like auth interceptors here
}
class ApiClient {
private axiosInstance: AxiosInstance;
constructor(options: ApiClientOptions) {
this.axiosInstance = axios.create({
baseURL: options.baseURL,
});
// Add interceptors here if needed (e.g., for authentication)
this.axiosInstance.interceptors.request.use(
(config) => {
// Modify config before request is sent
return config;
},
(error) => {
// Handle request error
return Promise.reject(error);
}
);
this.axiosInstance.interceptors.response.use(
(response) => {
// Modify response before it's returned
return response;
},
(error) => {
// Handle response error
return Promise.reject(error);
}
);
}
private async request<
Path extends keyof paths,
Method extends keyof paths[Path],
ResponseData = paths[Path][Method] extends { responses: { 200: { content: { 'application/json': infer T } } } } ? paths[Path][Method]['responses'][200]['content']['application/json'] : any,
RequestBody = paths[Path][Method] extends { requestBody: { content: { 'application/json': infer T } } } ? paths[Path][Method]['requestBody']['content']['application/json'] : any,
PathParams = paths[Path][Method] extends { parameters: { path: infer T } } ? T : never,
QueryParams = paths[Path][Method] extends { parameters: { query: infer T } } ? T : never
>(
path: Path,
method: Method,
data?: RequestBody,
params?: {
pathParams?: PathParams;
queryParams?: QueryParams;
}
): Promise<ResponseData> {
const config: AxiosRequestConfig = {
method: method,
url: this.replacePathParams(String(path), params?.pathParams),
params: params?.queryParams,
data: data,
};
try {
const response = await this.axiosInstance.request(config);
return response.data as ResponseData;
} catch (error: any) {
// Handle error based on the API's error structure
throw error;
}
}
private replacePathParams(path: string, pathParams: any): string {
if (!pathParams) {
return path;
}
let updatedPath = path;
for (const key in pathParams) {
if (pathParams.hasOwnProperty(key)) {
updatedPath = updatedPath.replace(`{${key}}`, String(pathParams[key]));
}
}
return updatedPath;
}
// Example API call (replace with your actual API endpoints)
async getUsers(queryParams?: paths['/users']['get']['parameters']['query']): Promise<components['schemas']['User'][]> {
return this.request('/users', 'get', undefined, { queryParams });
}
async createUser(data: components['schemas']['User']): Promise<components['schemas']['User']> {
return this.request('/users', 'post', data);
}
async getUserById(userId: string): Promise<components['schemas']['User']> {
return this.request('/users/{userId}', 'get', undefined, { pathParams: {userId: userId} });
}
// Add more API calls here based on your OpenAPI document
}
export default ApiClient;
这个 ApiClient 类封装了 axios,并提供了一个 request 方法用于发送 HTTP 请求。 它还提供了一些示例 API 调用函数,你可以根据你的 OpenAPI 文档修改它们。 重要的是,它使用了从 types.ts 文件导入的类型定义,从而实现了类型安全。
代码解释:
paths和components类型: 从src/api/types.ts导入,包含 API 路径和数据结构的类型定义。request方法: 一个泛型方法,用于发送 HTTP 请求。它接收 API 路径、HTTP 方法、请求体和查询参数作为参数,并返回响应数据。 使用了大量的类型推断,确保请求和响应的数据类型与 OpenAPI 文档中定义的类型一致。replacePathParams方法: 替换路径参数,例如/users/{userId}中的{userId}。- 示例 API 调用函数: 例如
getUsers、createUser和getUserById,展示了如何使用request方法来调用具体的 API 接口。 这些函数使用了从paths和components类型中提取的类型信息,提供了类型安全保证。
7. 使用 API 客户端
在 Vue 组件中使用 API 客户端:
<template>
<div>
<h1>Users</h1>
<ul>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="fetchUsers">Fetch Users</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
import ApiClient from '@/api/client';
import { components } from '@/api/types';
export default defineComponent({
name: 'UserList',
setup() {
const users = ref<components['schemas']['User'][]>([]);
const apiClient = new ApiClient({ baseURL: 'http://localhost:8080/api/v1' });
const fetchUsers = async () => {
try {
const fetchedUsers = await apiClient.getUsers();
users.value = fetchedUsers;
} catch (error) {
console.error('Failed to fetch users:', error);
}
};
onMounted(() => {
fetchUsers();
});
return {
users,
fetchUsers,
};
},
});
</script>
这个组件在 onMounted 钩子函数中调用 apiClient.getUsers() 方法来获取用户列表,并将结果显示在页面上。 由于使用了类型定义,IDE 会提供类型提示和代码补全,减少出错的可能性。
8. 错误处理
在 ApiClient 类中的 request 方法中,我们应该处理 API 请求可能发生的错误。根据你的 API 的错误结构,你可以提取错误信息并进行相应的处理。例如:
// In the ApiClient class, inside the request method:
try {
const response = await this.axiosInstance.request(config);
return response.data as ResponseData;
} catch (error: any) {
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error("Response data:", error.response.data);
console.error("Response status:", error.response.status);
console.error("Response headers:", error.response.headers);
// You can throw a custom error with the error message from the API
throw new Error(error.response.data.message || 'API error');
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error("No response received:", error.request);
throw new Error('No response from the server');
} else {
// Something happened in setting up the request that triggered an Error
console.error("Error setting up the request:", error.message);
throw new Error('Request setup error');
}
}
在 Vue 组件中,你可以使用 try...catch 语句来捕获 API 调用可能抛出的错误,并向用户显示友好的错误信息。
9. 认证与授权
如果你的 API 需要认证和授权,你需要在 ApiClient 类中添加相应的逻辑。 常见的认证方式包括:
- API Key: 在请求头或查询参数中传递 API Key。
- Bearer Token (JWT): 在请求头中传递 JWT token。
- OAuth 2.0: 使用 OAuth 2.0 协议获取 access token,并在请求头中传递。
你可以使用 axios 的 interceptors 来添加认证信息到请求头中。例如,对于 Bearer Token 认证:
// In the ApiClient constructor:
this.axiosInstance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('authToken'); // 获取 token
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
10. 代码生成流程自动化
为了简化开发流程,你可以使用 husky 和 lint-staged 等工具来自动化代码生成流程。 husky 可以在 Git hooks 中执行脚本,lint-staged 可以只对暂存区中的文件执行脚本。
首先,安装 husky 和 lint-staged:
npm install -D husky lint-staged
然后,配置 husky 和 lint-staged:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.{js,ts,vue}": [
"npm run generate-api-types",
"eslint --fix",
"prettier --write"
]
}
}
这个配置表示,在每次提交代码之前,lint-staged 会对暂存区中的 JavaScript、TypeScript 和 Vue 文件执行以下操作:
- 运行
npm run generate-api-types生成 API 类型定义。 - 使用
eslint --fix修复代码风格问题。 - 使用
prettier --write格式化代码。
最后,启用 husky:
npx husky install
现在,每次提交代码时,都会自动生成 API 类型定义,并修复代码风格问题。
11. 替代方案:OpenAPI Generator
虽然我们主要介绍了使用 openapi-typescript 和自定义脚本的方案,但 OpenAPI Generator 也是一个强大的选择。 它支持生成多种编程语言的 API 客户端代码,包括 TypeScript。
使用 OpenAPI Generator 的步骤:
-
安装 OpenAPI Generator:
npm install @openapitools/openapi-generator-cli -g -
生成代码:
openapi-generator generate -i src/api/openapi.json -g typescript-axios -o src/api/generated-i: 指定 OpenAPI 文档的路径。-g: 指定代码生成器。typescript-axios是一个常用的 TypeScript 代码生成器。-o: 指定输出目录。
-
配置和使用生成的代码:
OpenAPI Generator会生成大量的代码,包括 API 类、模型类和配置类。 你需要根据你的项目需求配置这些代码,并在你的 Vue 组件中使用它们。
OpenAPI Generator 的优点:
- 功能强大,支持多种编程语言和框架。
- 可以生成完整的 API 客户端代码,包括 API 类、模型类和配置类。
OpenAPI Generator 的缺点:
- 生成的代码量较大,可能需要进行大量的配置和修改。
- 灵活性较低,难以定制代码生成过程。
12. 代码示例
以下是一个简化的代码示例,展示了如何使用 openapi-typescript 和自定义脚本生成 API 类型定义和客户端代码,并在 Vue 组件中使用它们:
src/api/openapi.json:
{
"openapi": "3.0.0",
"info": {
"title": "Example API",
"version": "1.0.0"
},
"paths": {
"/users": {
"get": {
"summary": "Get all users",
"responses": {
"200": {
"description": "Successful operation",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
},
"post": {
"summary": "Create a new user",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"responses": {
"201": {
"description": "User created successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
}
},
"required": [
"name",
"email"
]
}
}
}
}
scripts/generate-api-types.js:
const { generate } = require('openapi-typescript');
const fs = require('fs').promises;
const path = require('path');
async function generateApiTypes() {
const openApiFilePath = path.resolve(__dirname, '../src/api/openapi.json');
const outputFilePath = path.resolve(__dirname, '../src/api/types.ts');
try {
const schema = await fs.readFile(openApiFilePath, 'utf-8');
const types = await generate(schema);
await fs.writeFile(outputFilePath, types);
console.log(`Successfully generated API types at ${outputFilePath}`);
} catch (error) {
console.error('Failed to generate API types:', error);
}
}
generateApiTypes();
src/api/types.ts: (由 scripts/generate-api-types.js 生成)
export interface components {
schemas: {
/** User */
User: {
id?: number;
name: string;
email: string;
};
};
}
export interface operations {}
export interface external {}
src/api/client.ts:
import axios, { AxiosInstance } from 'axios';
import { components } from './types';
class ApiClient {
private axiosInstance: AxiosInstance;
constructor(baseURL: string) {
this.axiosInstance = axios.create({
baseURL: baseURL,
});
}
async getUsers(): Promise<components['schemas']['User'][]> {
const response = await this.axiosInstance.get<components['schemas']['User'][]>('/users');
return response.data;
}
async createUser(user: components['schemas']['User']): Promise<components['schemas']['User']> {
const response = await this.axiosInstance.post<components['schemas']['User']>('/users', user);
return response.data;
}
}
export default ApiClient;
src/components/UserList.vue:
<template>
<div>
<h1>Users</h1>
<ul>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="fetchUsers">Fetch Users</button>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from 'vue';
import ApiClient from '@/api/client';
import { components } from '@/api/types';
export default defineComponent({
name: 'UserList',
setup() {
const users = ref<components['schemas']['User'][]>([]);
const apiClient = new ApiClient('http://localhost:3000');
const fetchUsers = async () => {
const fetchedUsers = await apiClient.getUsers();
users.value = fetchedUsers;
};
onMounted(() => {
fetchUsers();
});
return {
users,
fetchUsers,
};
},
});
</script>
13. 集成带来的好处
集成 OpenAPI/Swagger 文档到 Vue 应用中,可以带来以下好处:
- 自动化代码生成: 减少手动编写 API 调用代码的工作量,提高开发效率。
- 类型安全: TypeScript 类型定义可以帮助你在编译时发现类型错误,减少运行时错误。
- 代码一致性: 生成的代码遵循统一的规范,提高代码可读性和可维护性。
- API 文档同步: API 文档和代码保持同步,减少文档维护成本。
- 更好的开发体验: IDE 可以提供类型提示和代码补全,提高开发体验。
自动化流程,减少手动维护
通过集成 OpenAPI/Swagger 文档到 Vue 应用中,我们可以自动化生成 API 客户端代码和类型定义,从而提高开发效率和代码质量。
类型安全和代码一致性
使用生成的 TypeScript 类型定义,可以帮助我们在编译时发现类型错误,保证代码类型安全,并使代码风格保持一致。
认证与授权的集成
如果你的 API 需要认证和授权,你需要在 API 客户端中添加相应的逻辑,例如使用 axios 的 interceptors 来添加认证信息到请求头中。
更多IT精英技术系列讲座,到智猿学院