跳到主要内容

Vue 大型项目架构实战

Vue 大型项目设计

1. 目录结构设计

src/
api/ # API 层:请求封装、类型定义
modules/
user.ts
product.ts
request.ts # Axios 封装、拦截器
types.ts # 响应类型
assets/ # 静态资源
components/ # 通用组件
base/ # 基础组件(Button、Input、Modal)
business/ # 业务组件
composables/ # 可复用的 composition functions
directives/ # 自定义指令
layouts/ # 布局组件
router/ # 路由配置
modules/ # 按模块分割路由
guards.ts # 导航守卫
stores/ # Pinia stores
modules/
utils/ # 工具函数
views/ # 页面组件(对应路由)

2. 请求层设计

// request.ts:统一 Axios 封装
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'

class Request {
private instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000
})

constructor() {
this.setupInterceptors()
}

private setupInterceptors() {
// 请求拦截:注入 token
this.instance.interceptors.request.use(config => {
const token = useAuthStore().token
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})

// 响应拦截:统一错误处理
this.instance.interceptors.response.use(
(res: AxiosResponse<ApiResponse>) => {
if (res.data.code !== 0) {
showError(res.data.message)
return Promise.reject(res.data)
}
return res.data.data
},
async error => {
if (error.response?.status === 401) {
await useAuthStore().refreshToken()
return this.instance(error.config) // 重试
}
return Promise.reject(error)
}
)
}

get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T>(url: string, data?: unknown): Promise<T> {
return this.instance.post(url, data)
}
}

export const request = new Request()

3. 权限系统设计

// 基于 RBAC 的权限体系
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const permissions = ref<string[]>([])
const roles = ref<string[]>([])

function hasPermission(perm: string) {
return permissions.value.includes(perm)
}

function hasRole(role: string) {
return roles.value.includes(role)
}

return { permissions, roles, hasPermission, hasRole }
})

// composables/usePermission.ts
export function usePermission(permission: string) {
const store = useAuthStore()
return computed(() => store.hasPermission(permission))
}

// 自定义指令 v-permission
app.directive('permission', {
mounted(el, binding) {
const store = useAuthStore()
if (!store.hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})

// 路由守卫
router.beforeEach(async (to, from) => {
const store = useAuthStore()
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.permission && !store.hasPermission(to.meta.permission)) {
return { name: '403' }
}
})

4. 动态路由(按权限加载)

// 登录后根据权限动态注册路由
async function setupDynamicRoutes(permissions: string[]) {
const allRoutes = import.meta.glob('./modules/*.ts', { eager: true })
const accessibleRoutes = filterRoutesByPermission(allRoutes, permissions)
accessibleRoutes.forEach(route => router.addRoute(route))
}

// 刷新页面后路由恢复(动态路由会在刷新后丢失)
router.beforeEach(async (to) => {
const store = useAuthStore()
if (store.isLoggedIn && !store.routesLoaded) {
await store.loadUserRoutes() // 重新加载路由
return to.fullPath // 重新导航到目标路由
}
})

5. 组件通信规范

父子通信:props + emits(类型安全)
跨层级:provide / inject + readonly 包裹
兄弟/跨页:Pinia store
复杂异步:组合 composables
// 类型安全的 provide/inject
const FormContextKey = Symbol() as InjectionKey<FormContext>

// 父组件
provide(FormContextKey, readonly(formContext))

// 子组件
const form = inject(FormContextKey)
if (!form) throw new Error('FormContext not provided')

6. 性能优化实践

// 1. 路由懒加载(分包)
const Dashboard = () => import('./views/Dashboard.vue')

// 2. 组件异步加载(大型业务组件)
const HeavyTable = defineAsyncComponent({
loader: () => import('./HeavyTable.vue'),
loadingComponent: Skeleton,
errorComponent: ErrorView,
delay: 200,
timeout: 3000
})

// 3. 长列表虚拟滚动
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
items,
{ itemHeight: 50, overscan: 10 }
)

// 4. 计算属性缓存敏感数据
const expensiveData = computed(() => {
return heavyCalculation(rawData.value)
})

// 5. shallowRef 减少深层响应式
const tableData = shallowRef<Row[]>([]) // 不对数组元素建立深层响应

7. 资深面试题

  • Vue 大型项目如何解决 TypeScript 类型推导问题?
    • Volar 插件提供组件 props 类型推导
    • defineProps<{ title: string }>() 泛型语法直接推导类型
    • API 响应类型用 Zod 做运行时校验 + 类型推导
  • Pinia 如何在 SSR 中避免状态污染?
    • 每次请求创建新的 Pinia 实例,不共享服务端状态
    • useStore(pinia) 传入请求绑定的 pinia 实例
  • 如何做 Vue 项目的国际化?
    • vue-i18n 管理翻译文件,按语言懒加载
    • 日期、数字用 Intl API 处理本地化格式
    • 配合 CI/CD 自动提取新增翻译 key