Nuxt: Composables 组合式函数
最后更新:2026-08-26
Charlie 的团队有大量重复逻辑——价格格式化在 10 个组件里各写一遍,搜索逻辑散落在 3 个页面。Alice 的收藏功能需要跨页面共享状态。Bob 需要一个通用的分页逻辑。Composable 是解药——提取逻辑为可复用函数。
1. 你将学到
- Nuxt 3 内置 Composables 全览
- 自定义 Composable:目录约定与命名规范
- Composable 设计模式:异步/参数响应式/返回值解构
- Composable 组合:Composable 调用 Composable
- MegaShop:useProductSearch/usePriceFormat/useWishlist
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 依赖关系图
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,每次调用创建独立状态。选择取决于需求。
📖 小节
- Nuxt 3 内置 20+ Composables:useFetch/useState/useCookie/useRouter 等
- 自定义 Composable 放在 composables/ 下,以 use 前缀命名,自动导入
- 设计模式:同步(格式化)、异步(数据获取)、参数响应式(搜索)、组合(结账)
- Composable 可调用其他 Composable,实现复杂逻辑分层
- MegaShop 用 usePriceFormat/useWishlist/useProductSearch/useCheckout 构建业务逻辑层
📝 作业
- 基础题(难度⭐):创建 usePriceFormat Composable,在不同组件中格式化价格为 USD/JPY/CNY
- 进阶题(难度⭐⭐):创建 useProductSearch,实现关键词搜索 + 分类筛选 + 分页,验证 watch 参数变化自动刷新
- 挑战题(难度⭐⭐⭐):创建 useCheckout 组合 Composable,集成购物车 Store + 价格格式化 + 订单提交,实现完整的结算流程
---|