Vue应用中的构建流程与后端API文档(OpenAPI/Swagger)的集成:实现代码生成与类型安全

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 包(如 axiosopenapi-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.jsonhttp://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 文件导入的类型定义,从而实现了类型安全。

代码解释:

  • pathscomponents 类型: 从 src/api/types.ts 导入,包含 API 路径和数据结构的类型定义。
  • request 方法: 一个泛型方法,用于发送 HTTP 请求。它接收 API 路径、HTTP 方法、请求体和查询参数作为参数,并返回响应数据。 使用了大量的类型推断,确保请求和响应的数据类型与 OpenAPI 文档中定义的类型一致。
  • replacePathParams 方法: 替换路径参数,例如 /users/{userId} 中的 {userId}
  • 示例 API 调用函数: 例如 getUserscreateUsergetUserById,展示了如何使用 request 方法来调用具体的 API 接口。 这些函数使用了从 pathscomponents 类型中提取的类型信息,提供了类型安全保证。

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. 代码生成流程自动化

为了简化开发流程,你可以使用 huskylint-staged 等工具来自动化代码生成流程。 husky 可以在 Git hooks 中执行脚本,lint-staged 可以只对暂存区中的文件执行脚本。

首先,安装 huskylint-staged

npm install -D husky lint-staged

然后,配置 huskylint-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 文件执行以下操作:

  1. 运行 npm run generate-api-types 生成 API 类型定义。
  2. 使用 eslint --fix 修复代码风格问题。
  3. 使用 prettier --write 格式化代码。

最后,启用 husky

npx husky install

现在,每次提交代码时,都会自动生成 API 类型定义,并修复代码风格问题。

11. 替代方案:OpenAPI Generator

虽然我们主要介绍了使用 openapi-typescript 和自定义脚本的方案,但 OpenAPI Generator 也是一个强大的选择。 它支持生成多种编程语言的 API 客户端代码,包括 TypeScript。

使用 OpenAPI Generator 的步骤:

  1. 安装 OpenAPI Generator:

    npm install @openapitools/openapi-generator-cli -g
  2. 生成代码:

    openapi-generator generate -i src/api/openapi.json -g typescript-axios -o src/api/generated
    • -i: 指定 OpenAPI 文档的路径。
    • -g: 指定代码生成器。 typescript-axios 是一个常用的 TypeScript 代码生成器。
    • -o: 指定输出目录。
  3. 配置和使用生成的代码:

    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精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注