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