在刚开始学习Vue的时候,我通常会直接在组件里发请求。

比如:

const users = await fetch('/api/users')

项目小的时候这么写完全没问题。

但随着页面越来越多,请求逻辑开始重复,鉴权、错误处理、请求参数、响应类型这些问题也慢慢冒出来了。

这时候就有必要把API请求从组件里抽出来,并封装成可复用函数。如下文章内的getUsers函数:

TypeScript在前端项目中的实际使用:不要为了类型而类型

别让组件直接处理HTTP细节

假设项目里有一个用户列表页面。

最直接的写法可能是:

const loadUsers = async () => {
  const response = await fetch('/api/users')
  const data = await response.json()

  users.value = data
}

如果其他页面也要用用户数据,很快就到处是重复代码。

更合理的做法是建一个API层:

src/
├── api/
│   ├── user.ts
│   └── auth.ts
├── components/
├── views/
└── types/

比如:

export interface User {
  id: number
  name: string
  email: string
}

然后:

export async function getUsers(): Promise<User[]> {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('请求失败')
  }

  return response.json()
}

组件只需要:

const users = ref<User[]>([])

const loadUsers = async () => {
  users.value = await getUsers()
}

这样组件就不用关心接口地址了。

再封装一层请求客户端

如果每个API都重复写:

fetch(...)
response.ok
response.json()

还是有点啰嗦。

可以再封装一层:

export async function request<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const response = await fetch(url, options)

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }

  return response.json()
}

然后:

export function getUsers() {
  return request<User[]>('/api/users')
}

泛型在这里就派上用场了。

调用:

const users = await getUsers()

TypeScript能推导出users是:

User[]

统一处理鉴权

实际项目里,请求通常都要带Token。

可以把Token处理放到请求层。

比如:

const token = localStorage.getItem('token')

const headers = {
  'Content-Type': 'application/json',
  ...(token
    ? { Authorization: `Bearer ${token}` }
    : {})
}

这样业务API就不用每次都自己处理鉴权。

以后鉴权方式变了,也只需要改请求层。

错误处理也应该统一

接口请求失败一般有几种情况:

网络错误
HTTP 4xx
HTTP 5xx
业务错误
Token失效

如果每个页面各自处理,很快就会出现不同页面提示不一样的问题。

所以可以在请求层统一处理基础错误。

比如:

if (response.status === 401) {
  // 清理登录状态
  // 跳转登录页
}

业务代码主要关心:

try {
  users.value = await getUsers()
} catch (error) {
  console.error(error)
}

API类型和组件类型别混在一起

有时候后端返回的数据和前端实际用的数据并不完全一样。

比如后端:

interface UserResponse {
  user_id: number
  user_name: string
}

而前端希望:

interface User {
  id: number
  name: string
}

那就可以在API层做转换:

function normalizeUser(data: UserResponse): User {
  return {
    id: data.user_id,
    name: data.user_name
  }
}

这样可以避免后端数据结构直接渗透到整个前端项目里。

API层最重要的不是"封装"

我以前很容易陷入一个误区:

看到重复代码就想封装。

但封装本身不是目的。

API层真正解决的是:

组件
 ↓
业务逻辑
 ↓
API
 ↓
HTTP

每一层只关心自己的事。

组件关心UI。

业务逻辑关心状态。

API层关心接口。

HTTP层关心网络请求。

项目越大,这种边界带来的维护成本差异就越明显。

最终结构

一个中等规模的Vue项目,我比较倾向:

src/
├── api/
│   ├── client.ts
│   ├── auth.ts
│   └── user.ts
│
├── composables/
│   └── useUsers.ts
│
├── types/
│   └── user.ts
│
├── components/
│
└── views/

组件:

const {
  users,
  loading,
  refresh
} = useUsers()

Composable:

const users = ref<User[]>([])

async function refresh() {
  users.value = await getUsers()
}

API:

export function getUsers() {
  return request<User[]>('/api/users')
}

每一层的职责都比较清楚。

对我来说,这种结构也是Vue项目从"小项目能跑"慢慢走向"能长期维护"的一个重要转折点。

最后修改:2026 年 09 月 02 日
如果觉得我的文章对你有用,请随意赞赏