Nuxt: Composables 组合式函数

最后更新:2026-08-26

Charlie 的团队有大量重复逻辑——价格格式化在 10 个组件里各写一遍,搜索逻辑散落在 3 个页面。Alice 的收藏功能需要跨页面共享状态。Bob 需要一个通用的分页逻辑。Composable 是解药——提取逻辑为可复用函数。

1. 你将学到


2. 一个架构师的真实故事

(1) 痛点:重复逻辑散落各处

MegaShop 有 10 个组件需要格式化价格为 "USD 2,999.99" 格式,每个都自己写 Intl.NumberFormat。Alice 在首页和详情页都实现了收藏功能,但逻辑不一致——首页收藏了详情页却没显示。

(2) Composable 的解法

提取为 Composable 后,一处定义处处复用:

TYPESCRIPT
// composables/usePriceFormat.ts
export function usePriceFormat(price: Ref<number>, currency = 'USD') {
  return computed(() => new Intl.NumberFormat('en-US', {
    style: 'currency', currency
  }).format(price.value))
}

(3) 收益:DRY + 一致性

10 个组件共享同一个价格格式化逻辑,收藏状态通过 useWishlist 在所有页面保持一致。代码量减少 60%,bug 减少 80%。


3. 内置 Composables 全览

(1) 内置 Composable 依赖关系图

100%
graph TB
    A[Nuxt Built-in Composables] --> B[Data Fetching]
    A --> C[State Management]
    A --> D[Navigation]
    A --> E[Head Management]
    A --> F[Context]
    A --> G[Utility]

    B --> B1[useFetch]
    B --> B2[useAsyncData]
    B --> B3[$fetch]
    C --> C1[useState]
    C --> C2[useCookie]
    D --> D1[useRouter]
    D --> D2[useRoute]
    D --> D3[navigateTo]
    E --> E1[useHead]
    E --> E2[useSeoMeta]
    F --> F1[useRequestHeaders]
    F --> F2[useRuntimeConfig]
    G --> G1[useAppConfig]
    G --> G2[useHydration]

(2) 核心内置 Composables 速查

Composable 用途 SSR 可用 返回值
useFetch 数据获取 { data, pending, error, refresh }
useAsyncData 通用异步 { data, pending, error, refresh }
useState 共享状态 Ref
useCookie Cookie 操作 Ref
useRouter 路由实例 ⚠️ 仅客户端 Router
useRoute 当前路由 Route
useHead Head 管理 void
useSeoMeta SEO 元数据 void
useRuntimeConfig 运行时配置 RuntimeConfig
useRequestHeaders 请求头 ✅ 仅服务端 Headers

▶ 示例:useState 跨组件状态共享

VUE
<script setup lang="ts">
// Share notification state across components
const notifications = useState<Notification[]>('notifications', () => [])

function addNotification(message: string, type: 'success' | 'error' = 'success') {
  notifications.value.push({ id: Date.now(), message, type })
  setTimeout(() => {
    notifications.value = notifications.value.filter(n => n.id !== Date.now())
  }, 3000)
}
</script>

输出:

TEXT 📖 仅展示
// 执行成功

4. 自定义 Composable

(1) 命名与目录约定

规则 说明 示例
目录 composables/ composables/useCart.ts
命名 use 前缀 usePriceFormat
导出 命名导出 export function usePriceFormat()
类型 接口同文件定义 interface PriceOptions {}
自动导入 ✅ 自动 无需手动 import

▶ 示例:usePriceFormat 价格格式化

TYPESCRIPT
// composables/usePriceFormat.ts
interface PriceFormatOptions {
  currency?: string
  locale?: string
  showDecimals?: boolean
}

export function usePriceFormat(
  price: Ref<number> | number,
  options: PriceFormatOptions = {}
) {
  const { currency = 'USD', locale = 'en-US', showDecimals = true } = options

  const formatted = computed(() => {
    const value = unref(price)
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency,
      minimumFractionDigits: showDecimals ? 2 : 0,
      maximumFractionDigits: showDecimals ? 2 : 0
    }).format(value)
  })

  return { formatted }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:useWishlist 收藏夹

TYPESCRIPT
// composables/useWishlist.ts
export function useWishlist() {
  const wishlist = useState<number[]>('wishlist', () => [])

  function toggle(productId: number) {
    const index = wishlist.value.indexOf(productId)
    if (index === -1) {
      wishlist.value.push(productId)
    } else {
      wishlist.value.splice(index, 1)
    }
  }

  function isWishlisted(productId: number): boolean {
    return wishlist.value.includes(productId)
  }

  const count = computed(() => wishlist.value.length)

  return { wishlist, toggle, isWishlisted, count }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例:useProductSearch 搜索逻辑

TYPESCRIPT
// composables/useProductSearch.ts
interface SearchParams {
  query?: string
  category?: string
  minPrice?: number
  maxPrice?: number
  sortBy?: 'price' | 'name' | 'rating'
  page?: number
}

export function useProductSearch(initialParams: SearchParams = {}) {
  const params = reactive<SearchParams>({
    query: '',
    category: '',
    minPrice: 0,
    maxPrice: 10000,
    sortBy: 'rating',
    page: 1,
    ...initialParams
  })

  const { data, pending, error, refresh } = useFetch('/api/products/search', {
    query: params,
    watch: [params],
    default: () => ({ items: [], total: 0 })
  })

  function resetFilters() {
    params.query = ''
    params.category = ''
    params.minPrice = 0
    params.maxPrice = 10000
    params.sortBy = 'rating'
    params.page = 1
  }

  return { params, data, pending, error, refresh, resetFilters }
}

输出:

TEXT 📖 仅展示
// 执行成功

5. Composable 设计模式

(1) 设计模式对比

模式 特点 适用场景 示例
同步 Composable 返回 computed/ref 格式化/计算 usePriceFormat
异步 Composable 内部调用 useFetch 数据获取 useProductSearch
参数响应式 watch 参数自动刷新 筛选/搜索 useProductSearch
状态共享 useState 内部 跨组件状态 useWishlist
Composable 组合 调用其他 Composable 复杂逻辑 useCheckout

▶ 示例:Composable 组合——useCheckout

TYPESCRIPT
// composables/useCheckout.ts
export function useCheckout() {
  // Compose other composables
  const cartStore = useCartStore()
  const { formatted: total } = usePriceFormat(
    computed(() => cartStore.totalPrice)
  )
  const { isAuthenticated, user } = storeToRefs(useUserStore())

  const isProcessing = ref(false)
  const orderId = ref<string | null>(null)

  async function processCheckout() {
    if (!isAuthenticated.value) {
      navigateTo('/login')
      return
    }

    isProcessing.value = true
    try {
      const order = await $fetch('/api/orders', {
        method: 'POST',
        body: {
          items: cartStore.items,
          userId: user.value?.id,
          total: cartStore.totalPrice
        }
      })
      orderId.value = order.id
      cartStore.clearCart()
      navigateTo(`/orders/${order.id}`)
    } catch (err) {
      console.error('Checkout failed:', err)
    } finally {
      isProcessing.value = false
    }
  }

  return { total, isProcessing, orderId, processCheckout, isAuthenticated }
}

输出:

TEXT 📖 仅展示
// 执行成功

(2) Composable 设计原则

原则 说明 反例
单一职责 一个 Composable 做一件事 useShopAndCart()
参数响应式 接收 Ref 参数 只接收原始值
返回值解构 返回命名对象 返回数组
命名清晰 use + 动词/名词 doStuff()
无副作用 不直接修改 DOM 内部操作 DOM

6. 综合示例:MegaShop Composable 体系

TYPESCRIPT
// composables/usePagination.ts
export function usePagination(totalItems: Ref<number>, pageSize = 20) {
  const route = useRoute()
  const router = useRouter()

  const currentPage = computed(() => Number(route.query.page) || 1)
  const totalPages = computed(() => Math.ceil(totalItems.value / pageSize))
  const hasNext = computed(() => currentPage.value < totalPages.value)
  const hasPrev = computed(() => currentPage.value > 1)

  function goToPage(page: number) {
    router.push({ query: { ...route.query, page: String(page) } })
  }

  function nextPage() {
    if (hasNext.value) goToPage(currentPage.value + 1)
  }

  function prevPage() {
    if (hasPrev.value) goToPage(currentPage.value - 1)
  }

  const offset = computed(() => (currentPage.value - 1) * pageSize)

  return { currentPage, totalPages, hasNext, hasPrev, offset, goToPage, nextPage, prevPage }
}
TYPESCRIPT
// composables/useNotification.ts
interface Notification { id: number; message: string; type: 'success' | 'error' | 'info' }

export function useNotification() {
  const notifications = useState<Notification[]>('notifications', () => [])

  function notify(message: string, type: Notification['type'] = 'success') {
    const id = Date.now()
    notifications.value = [...notifications.value, { id, message, type }]
    setTimeout(() => {
      notifications.value = notifications.value.filter(n => n.id !== id)
    }, 3000)
  }

  function dismiss(id: number) {
    notifications.value = notifications.value.filter(n => n.id !== id)
  }

  return { notifications, notify, dismiss }
}

❓ 常见问题

Q Composable 和 Pinia Store 怎么选?
A 需要跨组件持久状态用 Store(购物车/用户),逻辑复用用 Composable(格式化/搜索)。Composable 更轻量,Store 有 DevTools 支持。
Q Composable 里能用 useFetch 吗?
A 可以,这叫异步 Composable。调用时需要 await,就像在页面里一样。useProductSearch 就是这样设计的。
Q 为什么 Composable 必须以 use 开头?
A Nuxt 3 的 auto-import 只扫描 composables/ 下的文件,不强制 use 前缀。但 Vue 社区约定以 use 开头,便于识别和工具支持。
Q Composable 里能用 useState 吗?
A 可以。useState 创建的状态在 SSR/客户端自动水合,适合跨组件共享。useWishlist 就用 useState 存储。
Q Composable 的参数应该是 Ref 还是原始值?
A 推荐接收 Ref,这样参数变化时 Composable 内部可以响应式更新。用 unref() 同时兼容两种类型。
Q 多个组件调用同一个 Composable 会共享状态吗?
A 如果 Composable 内部用 useState,状态会共享。如果用 ref/ reactive,每次调用创建独立状态。选择取决于需求。

📖 小节


📝 作业

  1. 基础题(难度⭐):创建 usePriceFormat Composable,在不同组件中格式化价格为 USD/JPY/CNY
  2. 进阶题(难度⭐⭐):创建 useProductSearch,实现关键词搜索 + 分类筛选 + 分页,验证 watch 参数变化自动刷新
  3. 挑战题(难度⭐⭐⭐):创建 useCheckout 组合 Composable,集成购物车 Store + 价格格式化 + 订单提交,实现完整的结算流程

---|

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏