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-2/