4.3 composables封装与错误处理


4.3 composables 封装与错误处理

交接区的最后一课讲工程组织:取数逻辑散落在几十个组件里怎么办,接口失败时用户看到什么。本节把前两节的状态工具与第 3 章的取数 API 组装成可复用的数据层,并建立加载、错误、空三态的呈现规范——它是第 6 章 API 层的客户端对位,也是团队协作的接口契约。

从散装取数到数据层

没有数据层的项目长这样:商品页写一份 useFetch 加模板判断,订单页复制一份改改 URL,搜索页再来一份。三份代码三份空态处理三份错误分支,接口路径改一次,全局搜索替换。composables 目录的价值就是把这类逻辑收拢:

// composables/useProducts.ts // 商品数据的唯一入口:路径、加工、错误策略都定义在一处 export function useProducts(page: Ref<number>) { return useFetch('/api/products', { key: `products-page-${page.value}`, // 分页进 key,避免缓存串页 query: { page, size: 20 }, transform: (raw: any) => ({ // 原始响应到视图模型的加工 list: raw.items.map((i: any) => ({ id: i.id, name: i.title, price: (i.priceCents / 100).toFixed(2), })), total: raw.total, }), default: () => ({ list: [], total: 0 }), }) } // 订单数据:另一个业务域,另一个 composable export function useOrders(status: Ref<string>) { return useFetch('/api/orders', { key: `orders-${status.value}`, query: { status }, default: () => ({ list: [] }), }) }

组件退化成纯消费方:

<!-- pages/products.vue --> <script setup> const page = ref(1) const { data, status, error } = useProducts(page) </script> <template> <ProductList :items="data.list" :loading="status === 'pending'" /> <button @click="page++; ">下一页</button> </template>

数据层封装的三条收益:接口变更只改一处;空态与错误策略统一;key 命名集中管理不再冲突。判断一个 composable 写得好不好,看组件还剩多少行——剩下的应该只有"取哪个数据、怎么展示"。

状态机式的三态设计

数据到位前后的界面状态,用 status 字段驱动而非多个布尔值拼凑。useFetch 返回的 status 是个小型状态机:idle → pending → success/error。模板按状态分段渲染:

<template> <!-- 加载:骨架屏比转圈体验好,SSR 页面首屏通常直接 success --> <ProductSkeleton v-if="status === 'pending'" /> <!-- 错误:区分可重试 --> <ErrorPanel v-else-if="error" :message="errorMessage" @retry="refresh" /> <!-- 空:不是错误,是正常业务态,给出引导 --> <EmptyGuide v-else-if="data.list.length === 0" text="还没有商品,去别处逛逛" /> <!-- 成功 --> <ProductGrid v-else :items="data.list" /> </template>

三条设计纪律:

  • 加载态优先骨架屏:保持布局稳定,避免内容跳动(CLS,第 8 章 Lighthouse 会考);
  • 错误态给出口:错误面板必带重试按钮,绑 refresh;纯文字"加载失败"是死胡同;
  • 空态是业务设计:空列表要有引导动作,别只给一句"暂无数据"。

错误的两条处理路径

第 2 章 error.vue 处理"没接住的错"。数据层的错误要主动设计走向:

路径一:局部呈现(非 fatal)。接口失败但页面骨架还在,展示错误面板:

<script setup> const { data, error, refresh } = await useFetch('/api/products').catch(() => ({})) // 或者显式检查 error,构造友好文案 const errorMessage = computed(() => { if (!error.value) return '' const s = error.value.statusCode if (s === 404) return '商品不存在或已下架' if (s >= 500) return '服务器开小差了,稍后再试' return '网络异常,请检查连接' }) </script>

路径二:整页接管(fatal)。页面核心数据失败,继续渲染没有意义:

<script setup> const { data, error } = await useFetch('/api/product-detail') if (error.value) { // fatal 错误交给 error.vue,状态码原样传递 throw createError({ statusCode: error.value.statusCode || 500, statusMessage: '商品加载失败', fatal: true, }) } </script>

判据一句话:这块数据没了页面还有没有意义——列表页某接口失败可以局部降级,详情页主数据失败就该整页接管。

变体:会话状态的组合式封装

数据层之外,composables 也适合封装"跨页面的会话逻辑"。一个带副作用的例子:

// composables/useToast.ts // 轻量提示:状态走 useState,行为就近定义 export function useToast() { const toasts = useState<{ id: number; text: string }[]>('toasts', () => []) function show(text: string) { const id = Date.now() toasts.value.push({ id, text }) // 定时器只该在客户端跑:判端保护 if (import.meta.client) { setTimeout(() => { toasts.value = toasts.value.filter(t => t.id !== id) }, 3000) } } return { toasts, show } }

注意判端保护:setTimeout 在服务端渲染路径上属于泄漏源(4.1 钩子一节的坑),副作用必须锁在客户端。这个例子同时示范了 4.1(useState)与本节(行为封装)的组合:状态跨端共享,行为两端可用,副作用单端执行。

⚠️ 常见坑:composables 里忘写 key 或 key 用了固定字符串接动态参数。useFetch 不给 key 时按代码位置生成,封装后调用位置变化会引发缓存失效;分页、筛选参数必须编进 key,否则翻页显示旧数据。

💡 关键直觉:数据层的本质是给"接口怎么调"立规矩——key 怎么命名、响应怎么加工、三态怎么呈现、错误走哪条路。规矩立在一处,组件才可能薄。

本节要点回顾

  • 数据层封装:按业务域一个 composable 一个函数,收拢路径、加工、key 与错误策略,组件只剩消费;
  • status 状态机:pending/error/empty/success 四段渲染,加载用骨架屏、错误给重试、空态给引导;
  • 错误双路径:局部降级(error 面板 + refresh)与整页接管(createError fatal → error.vue),按"页面还有没有意义"取舍;
  • 副作用判端:定时器等浏览器 API 锁在 import.meta.client 分支内;
  • key 编排:动态参数进 key,分页筛选不串页。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U