You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue3如何避免API调用代码重复?实现声明式类型安全API服务

Vue3 API调用优化:实现声明式类型安全链式调用方案

问题背景

当前Vue3项目采用的API调用流程存在高重复度问题:

  • 在api文件夹中为每个实体的CRUD编写独立函数,子资源(如练习、选项)需重复传入courseId等参数,代码结构高度雷同;
  • 通过Vuex Action调用API函数并将数据存入Store;
  • 组件通过访问Store状态、分发Action获取数据。

期望实现声明式定义路由与参数的类型安全链式调用,示例如下:

getApiService().courses(courseId).delete() // 删除课程
getApiService().courses(courseId).exercises().get(exerciseId) // 获取单个练习
getApiService().courses(courseId).exercises().post(payload) // 创建练习并返回结果
getApiService().courses(courseId).exercises().get(exerciseId).choices().list() // 获取练习的选项列表

解决方案

一、手动实现思路(类型安全链式API)

核心通过嵌套类结构+TypeScript泛型维护路由路径、参数类型与返回值类型,结合axios封装基础请求逻辑。

1. 封装基础API服务类

负责维护当前路径,提供通用CRUD方法,支持嵌套子资源:

import axios from 'axios';

// 基础API服务类,泛型T指定当前资源的返回类型
class ApiService<T> {
  private path: string;

  constructor(path: string = '') {
    this.path = path;
  }

  // 拼接子资源路径,返回新的ApiService实例
  protected resource<U>(name: string): ApiService<U> {
    return new ApiService<U>(`${this.path}/${name}`);
  }

  // 拼接带参数的资源路径(如/courses/:courseId)
  protected resourceWithParam<U>(name: string, param: string | number): ApiService<U> {
    return new ApiService<U>(`${this.path}/${name}/${param}`);
  }

  // 获取资源列表
  async list(): Promise<T[]> {
    const res = await axios.get(this.path);
    return res.data;
  }

  // 获取单个资源
  async get(id: string | number): Promise<T> {
    const res = await axios.get(`${this.path}/${id}`);
    return res.data;
  }

  // 创建资源
  async post(payload: Partial<T>): Promise<T> {
    const res = await axios.post(this.path, payload);
    return res.data;
  }

  // 更新资源
  async put(id: string | number, payload: Partial<T>): Promise<T> {
    const res = await axios.put(`${this.path}/${id}`, payload);
    return res.data;
  }

  // 删除资源(支持删除单个或当前路径指向的资源)
  async delete(id?: string | number): Promise<void> {
    const targetPath = id ? `${this.path}/${id}` : this.path;
    await axios.delete(targetPath);
  }
}

2. 定义业务嵌套资源类

继承基础类,声明子资源方法并指定类型:

import { Course, Exercise, Choice } from '@/types';

// 选项资源API
class ChoiceApi extends ApiService<Choice> {}

// 练习资源API,嵌套选项子资源
class ExerciseApi extends ApiService<Exercise> {
  choices(): ChoiceApi {
    return this.resource<Choice>('choices');
  }
}

// 课程资源API,嵌套练习子资源
class CourseApi extends ApiService<Course> {
  // 静态方法:生成带courseId的课程路径实例
  static courses(courseId: string | number): CourseApi {
    return new CourseApi(`/courses/${courseId}`);
  }

  exercises(): ExerciseApi {
    return this.resource<Exercise>('exercises');
  }
}

// 顶层API入口
export function getApiService() {
  return {
    courses: CourseApi.courses
  };
}

3. 实际调用示例

完全符合需求的类型安全链式调用:

// 删除指定课程
await getApiService().courses('course_123').delete();

// 获取指定课程下的单个练习
const exercise = await getApiService().courses('course_123').exercises().get('ex_456');

// 创建课程下的练习
const newExercise = await getApiService().courses('course_123').exercises().post({
  title: '新练习',
  content: '练习内容'
});

// 获取指定练习的选项列表
const choices = await getApiService().courses('course_123').exercises().get('ex_456').choices().list();

4. 结合Vuex的优化

在Vuex Action中直接调用链式API,减少重复代码:

// store/modules/courses.ts
import { getApiService } from '@/api/service';

const actions = {
  async fetchCourse({ commit }, courseId: string) {
    const course = await getApiService().courses(courseId).get();
    commit('SET_COURSE', course);
  },
  async createExercise({ commit }, { courseId, payload }) {
    const exercise = await getApiService().courses(courseId).exercises().post(payload);
    commit('ADD_EXERCISE', exercise);
  }
};

二、适用于Vue3的现有工具包

如果不想手动实现,可选择以下成熟工具:

  • Vue Query:作为数据获取缓存库,可与自定义链式API结合,替代Vuex处理缓存、自动重新请求等逻辑,减少Store冗余代码;
  • ts-rest:基于TypeScript的声明式API工具,通过Schema定义接口,自动生成类型安全的客户端调用代码,天生支持嵌套资源与链式调用;
  • OpenAPI Generator:若后端提供OpenAPI/Swagger文档,可直接生成TypeScript类型安全的API客户端,自动处理嵌套资源与参数,无需手动编写重复代码。

内容的提问来源于stack exchange,提问作者Samuele B.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 06:18:19