link,[object Object]
Skip to content

请求放哪层 ​

是什么 ​

「数据玄学」四大件:快速切换 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)——一次设计好,「数据玄学」少一半。

参考链接 ​