Vue3组合式API大型项目架构实践:从Composable设计到工程化分包

Vue3组合式API的项目组织方式

Vue3组合式API(Composition API)解决了选项式API在大型组件中逻辑分散的问题——相关的数据、计算属性、方法、生命周期钩子散落在data、computed、methods、mounted等选项中,维护时需要在多个选项之间反复跳转。组合式API允许将同一关注点的代码聚合在一起,以setup函数或<script setup>语法糖的形式组织。

大型项目中推荐将可复用逻辑抽取为组合函数(Composables),每个Composable封装一个独立的关注点,组件通过组合多个Composable搭建功能。这种组织方式让单个组件的代码量可控,逻辑边界清晰,测试友好。

Composable设计模式与最佳实践

编写Composable的核心原则:命名以use开头,接受ref或getter作为参数以保持响应式链路,返回包含响应式数据的对象。以下是一个处理分页查询的Composable,覆盖了加载状态、错误处理、防抖、数据缓存等常见需求。

// composables/usePagination.ts
import { ref, computed, watch, type Ref } from 'vue'

interface PaginationOptions {
  fetchFn: (page: number, pageSize: number) => Promise<any>
  pageSize?: number
  debounceMs?: number
}

interface PaginationState<T> {
  data: Ref<T[]>
  total: Ref<number>
  currentPage: Ref<number>
  pageSize: Ref<number>
  loading: Ref<boolean>
  error: Ref<Error | null>
  refresh: () => Promise<void>
  goToPage: (page: number) => void
  totalPages: Ref<number>
}

export function usePagination<T = any>(
  options: PaginationOptions
): PaginationState<T> {
  const data = ref<T[]>([]) as Ref<T[]>
  const total = ref(0)
  const currentPage = ref(1)
  const pageSize = ref(options.pageSize ?? 20)
  const loading = ref(false)
  const error = ref<Error | null>(null)

  const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

  let debounceTimer: ReturnType<typeof setTimeout> | null = null

  async function fetchData() {
    if (debounceTimer) clearTimeout(debounceTimer)

    await new Promise<void>((resolve) => {
      debounceTimer = setTimeout(resolve, options.debounceMs ?? 300)
    })

    loading.value = true
    error.value = null
    try {
      const result = await options.fetchFn(currentPage.value, pageSize.value)
      data.value = result.list
      total.value = result.total
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  function goToPage(page: number) {
    if (page < 1 || page > totalPages.value) return
    currentPage.value = page
  }

  // 页码变化自动刷新
  watch(currentPage, fetchData)

  // 初始加载
  fetchData()

  return {
    data,
    total,
    currentPage,
    pageSize,
    loading,
    error,
    refresh: fetchData,
    goToPage,
    totalPages,
  }
}

大型项目的目录结构设计

项目超过50个组件后,目录结构直接影响开发效率。推荐按功能模块组织而非按文件类型组织——同一功能模块的组件、Composable、类型定义、样式放在一起,减少跨目录跳转。

src/
├── modules/                    # 按业务模块组织
│   ├── user/
│   │   ├── components/         # 用户模块专用组件
│   │   │   ├── UserTable.vue
│   │   │   └── UserForm.vue
│   │   ├── composables/        # 用户模块Composable
│   │   │   ├── useUserList.ts
│   │   │   └── useUserForm.ts
│   │   ├── types.ts            # 用户模块类型定义
│   │   └── api.ts              # 用户模块API
│   ├── order/
│   │   ├── components/
│   │   ├── composables/
│   │   ├── types.ts
│   │   └── api.ts
│   └── dashboard/
│       └── ...
├── shared/                     # 跨模块共享
│   ├── components/             # 通用基础组件
│   ├── composables/            # 通用Composable
│   ├── utils/
│   └── types/
├── router/
├── stores/                     # Pinia全局Store
├── styles/                     # 全局样式与CSS变量
└── App.vue

Pinia状态管理与Store拆分策略

Pinia是Vue3官方推荐的状态管理库,相比Vuex去掉了mutations概念,API更简洁。大型项目中的关键问题不是Pinia怎么用,而是Store怎么拆。过度拆分导致Store碎片化,合在一起又会让单个Store臃肿。折中方案:按业务模块拆Store,每个Store不超过200行逻辑;跨模块共享状态放在独立的shared Store中。

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const currentUser = ref<User | null>(null)
  const token = ref<string | null>(localStorage.getItem('token'))

  const isLoggedIn = computed(() => !!token.value)
  const permissions = computed(() => currentUser.value?.roles?.flatMap(r => r.permissions) ?? [])

  async function login(credentials: LoginParams) {
    const res = await authApi.login(credentials)
    token.value = res.token
    currentUser.value = res.user
    localStorage.setItem('token', res.token)
  }

  function logout() {
    token.value = null
    currentUser.value = null
    localStorage.removeItem('token')
  }

  return { currentUser, token, isLoggedIn, permissions, login, logout }
})

Vue3组件通信模式对比

不同层级的组件间通信应选择不同方案。父子组件用props/emits,跨层级用provide/inject,全局状态用Pinia,兄弟组件通信走事件总线或Pinia。选错方案会导致代码冗余或响应式链路断裂。

provide/inject适合”深层传递”场景——祖父组件向孙子组件传数据,无需中间组件逐层转发。但inject的值默认不是响应式的,需要用ref或reactive包装后provide,接收端才能感知变更。TypeScript环境下推荐用InjectionKey类型化注入的key。

// 父组件提供主题
import { ref, provide, type InjectionKey, type Ref } from 'vue'

interface Theme {
  mode: 'light' | 'dark'
  primaryColor: string
}

const ThemeKey: InjectionKey<Ref<Theme>> = Symbol('theme')

// Provider组件
export default {
  setup() {
    const theme = ref<Theme>({
      mode: 'dark',
      primaryColor: '#409eff'
    })
    provide(ThemeKey, theme)
  }
}

// Consumer组件(任意层级子组件)
import { inject } from 'vue'

export default {
  setup() {
    const theme = inject(ThemeKey)
    if (!theme) throw new Error('Theme not provided')
    // theme.value.mode 是响应式的
  }
}

路由守卫与权限控制方案

大型项目的权限控制分为两个层面:路由级(哪些页面可以访问)和按钮级(页面内哪些操作可用)。路由级权限通过beforeEach守卫实现,在导航前检查用户角色是否匹配路由meta中声明的权限要求。按钮级权限通过自定义指令v-permission实现,在DOM渲染前判断是否移除元素。

// router/guard.ts
import type { Router } from 'vue-router'

export function setupPermissionGuard(router: Router) {
  router.beforeEach((to, from) => {
    const userStore = useUserStore()

    // 白名单路由直接放行
    if (to.meta.noAuth) return true

    // 未登录跳转登录页
    if (!userStore.isLoggedIn) {
      return { name: 'login', query: { redirect: to.fullPath } }
    }

    // 检查路由所需权限
    const required = to.meta.permissions as string[] | undefined
    if (required?.length) {
      const hasPermission = required.every(p => userStore.permissions.includes(p))
      if (!hasPermission) return { name: '403' }
    }

    return true
  })
}

// 自定义权限指令
// directives/permission.ts
import type { Directive } from 'vue'
import { useUserStore } from '@/stores/user'

export const vPermission: Directive<HTMLElement, string[]> = {
  mounted(el, binding) {
    const userStore = useUserStore()
    const required = binding.value
    if (!required) return
    const hasPermission = required.every(p => userStore.permissions.includes(p))
    if (!hasPermission) {
      el.parentNode?.removeChild(el)
    }
  }
}

前端工程化:Vite构建优化与分包策略

Vite在生产构建时默认将所有依赖打包到vendor chunk,大型项目中这会导致单个chunk过大(超过1MB),影响首屏加载速度。手动分包(manualChunks)将大型依赖拆分为独立chunk,利用浏览器并行下载和缓存机制优化加载性能。

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
          'echarts': ['echarts'],
          'utils': ['lodash-es', 'dayjs', 'axios'],
        }
      }
    },
    chunkSizeWarningLimit: 500,  // 超过500KB的chunk告警
    cssCodeSplit: true,          // CSS按组件拆分
  }
})

分包后配合路由懒加载,只在用户访问对应路由时才下载相关chunk,首屏资源体积可从2-3MB降到300-500KB。对静态资源开启gzip/brotli压缩,配置HTTP缓存头(js/css文件名含hash,设置强缓存;index.html不缓存),二次访问基本零网络开销。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-da-xing-xiang-mu-jia-gou-shi-jian-cong/

(0)
小编小编
上一篇 19小时前
下一篇 19小时前

相关推荐