Composable 模式

View source
业务 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/**'],
  },
})
或在 composable 中手动 import(更明确的依赖关系)。

分层职责

职责文件位置
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>
Copyright © 2025 - 2026 YiXuan - MIT License