外观
请求放哪层
是什么
「数据玄学」四大件:快速切换 tab 时列表数据串台、弹窗关了请求还在跑、同一个接口一进页面连发三次、报错只有 console 里一行红字。这四个问题都不是请求本身的错,是请求这层没设计。
请求层要回答四个问题:放在哪、竞态怎么办、重复了怎么办、错了怎么办。
对比:裸写在组件里 vs 封装成 composable
组件里裸写 onMounted + fetch | 封装成 useXxxData | |
|---|---|---|
| 写起来 | ⭐⭐⭐⭐ 两分钟搞定 | ⭐⭐ 多一个文件 |
| 第二个页面要用 | ⭐ 复制粘贴,bug 也跟着复制 | ⭐⭐⭐⭐⭐ import 就行 |
| 竞态/取消 | ⭐ 每处都得记得手写 | ⭐⭐⭐⭐ 写一次,处处生效 |
| 错误处理 | ⭐ 各写各的,风格漂移 | ⭐⭐⭐⭐ 统一进 toast / 重试 |
| 适用 | 一次性页面、用一次的请求 | 一切会活在两周以上的页面 |
结论:两分钟版只配活在 demo 里;正路是「api/ 放函数 + composable 管状态」,见目录分层约定。
四个设计点
1. 放哪
- 接口定义 →
api/(一个端点一个函数,只有 fetch) - 加载态、数据、刷新 → composable(
useUsers) - 组件只管调 composable,不直接碰 fetch——这条是目录纪律的推论
2. 竞态:新请求要能作废旧请求
经典事故:搜索框连续输「前」「前端」,两个请求同时飞,谁后回来谁上屏——输入快的反而显示旧结果。解法是给请求编号,渲染前核对身份:
ts
// composables/useSearch.ts —— 竞态守卫的最小形态
const results = ref([])
let seq = 0
async function search(keyword: string) {
const mySeq = ++seq // 每次搜索领一个新号码
const data = await fetchUsers(keyword)
if (mySeq !== seq) return // 已经有更新的搜索了,丢弃本次结果
results.value = data
}3. 取消:页面走了,请求也该走
编号守卫解决「别上屏」,AbortController 解决「别白跑」——路由切走时中止还在飞的请求:
ts
const controller = new AbortController()
onMounted(() => fetchUsers(keyword, { signal: controller.signal }))
onUnmounted(() => controller.abort()) // 离开页面,请求作废注意:abort 会让 promise 以错误收场,catch 里要放行 AbortError(见下面完整版)。
4. 错误:不许只有 console
错误分两种,处理方式不同:
- 用户能懂的(网络断了、没登录、没权限)→ 转成人话放界面/toast
- 用户不懂的(代码 bug、服务端 500)→ 提示「出错了,可重试」,留
console.error给自己
最小完整版
ts
// composables/useUsers.ts —— 四个设计点的最小合体
import { ref, onUnmounted, type Ref } from 'vue'
import { fetchUsers } from '@/api/user'
export function useUsers(keyword: Ref<string>) {
const users = ref([])
const loading = ref(false)
const error = ref('')
let seq = 0
let controller: AbortController | null = null
async function load() {
controller?.abort() // 上一个还没回来?作废
seq++
const mySeq = seq
controller = new AbortController()
loading.value = true
error.value = ''
try {
users.value = await fetchUsers(keyword.value, {
signal: controller.signal
})
} catch (e) {
if (e.name === 'AbortError') return // 主动取消不算错误
error.value = '加载失败,请重试'
console.error(e) // 人话给用户,原文给自己
} finally {
if (mySeq === seq) loading.value = false
}
}
onUnmounted(() => controller?.abort())
return { users, loading, error, load }
}结论一句话
请求四问——放哪(api + composable)、竞态(编号守卫)、取消(AbortController 跟卸载走)、错误(人话给用户、原文给 console)——一次设计好,「数据玄学」少一半。
参考链接
- Vue 官方 · 组合式函数
- web.dev · 网络性能——请求层之下的加载优化全景