在刚开始学习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项目从"小项目能跑"慢慢走向"能长期维护"的一个重要转折点。