Composable 模式
业务 Composable 层设计、薄 View 层理念和 useAsyncData 的 key 管理策略
Composable 层设计
核心思路:将数据获取和业务逻辑从 .vue 文件中抽离到 composable,.vue 文件只负责模板渲染和简单的 UI 交互。
目录结构
app/composables/ 下的顶层文件会被 Nuxt 自动导入(如 useApi.ts),但 modules/ 子目录下的不会。需要在 nuxt.config.ts 中配置:export default defineNuxtConfig({
imports: {
dirs: ['composables/modules/**'],
},
})
分层职责
| 层 | 职责 | 文件位置 |
|---|---|---|
| Shared 层 | 前后端共享的类型和纯工具函数 | shared/types/、shared/utils/ |
| API 层 | HTTP 请求封装、接口入参/出参类型 | app/api/modules/*/ |
| Composable 层 | 业务逻辑、状态管理、数据转换 | app/composables/modules/*/ |
| View 层 | 模板渲染、UI 交互、事件绑定 | app/pages/、app/components/ |
| Plugin 层 | $fetch 实例创建、全局配置 | app/plugins/ |
薄 View 层
一个理想的页面文件的 <script setup> 应当只包含 composable 调用和少量 UI 状态(如 dialog 开关)。所有数据获取和业务处理都在 composable 中完成。
完整示例
Composable 层 -- 封装列表数据获取、搜索、分页、删除等业务逻辑:
app/composables/modules/user/useUserList.ts
import type { UserListQuery } from '~/api/modules/user/types'
import { fetchUsers } from '~/api/modules/user'
// User 类型已在 shared/types/ 中定义,自动导入
export function useUserList() {
const query = ref<UserListQuery>({
page: 1,
pageSize: 20,
keyword: '',
})
const { data, status, refresh } = useApiFetch('/users', {
query,
watch: [query],
})
const users = computed(() => data.value?.items ?? [])
const total = computed(() => data.value?.total ?? 0)
const { $api } = useNuxtApp()
async function handleDelete(id: string) {
await $api(`/users/${id}`, { method: 'DELETE' })
await refresh()
}
function handlePageChange(page: number) {
query.value = { ...query.value, page }
}
function handleSearch(keyword: string) {
query.value = { ...query.value, keyword, page: 1 }
}
return {
query: readonly(query),
users,
total,
status,
handleDelete,
handlePageChange,
handleSearch,
}
}
View 层 -- 页面文件极度精简,只调用 composable 并绑定模板:
app/pages/users/index.vue
<script setup lang="ts">
const {
users,
total,
status,
query,
handleDelete,
handlePageChange,
handleSearch,
} = useUserList()
</script>
<template>
<div>
<UserSearchBar @search="handleSearch" />
<div v-if="status === 'pending'">
<USkeleton class="h-12 w-full" />
<USkeleton class="h-12 w-full" />
<USkeleton class="h-12 w-full" />
</div>
<UserTable
v-else
:users="users"
@delete="handleDelete"
/>
<UPagination
:total="total"
:page="query.page"
:page-size="query.pageSize"
@update:page="handlePageChange"
/>
</div>
</template>
当一个页面需要调用多个接口时,在 composable 中用
Promise.all 或多个 useApiFetch 组合,而非在页面文件中逐一调用。多接口聚合示例
app/composables/modules/dashboard/useDashboard.ts
// User、DashboardStats、Activity 等业务实体类型已在 shared/types/ 中定义,自动导入
export function useDashboard() {
const { $api } = useNuxtApp()
return useAsyncData('dashboard', async () => {
const [users, stats, activities] = await Promise.all([
$api<User[]>('/users', { query: { pageSize: 5 } }),
$api<DashboardStats>('/stats'),
$api<Activity[]>('/activity/recent'),
])
return { users, stats, activities }
})
}
app/pages/dashboard.vue
<script setup lang="ts">
const { data, status } = await useDashboard()
</script>