当项目规模从“几个页面”膨胀到“几十个模块、上百个组件、多人并行开发”时,架构设计就不再是锦上添花,而是决定项目能否持续迭代的命脉。大型 Vue 项目的架构核心在于分层解耦、职责清晰、可复用沉淀、全链路监控。
业务域拆分与模块分层
不要把所有代码平铺在 src 目录下。按业务领域进行一级划分,每个领域内部再按技术职责分层:
src/
├── modules/ # 业务域
│ ├── user/ # 用户域
│ │ ├── views/ # 页面组件
│ │ ├── components/ # 域内业务组件
│ │ ├── api/ # 接口层
│ │ ├── store/ # 状态管理(Pinia)
│ │ └── utils/ # 域内工具函数
│ ├── order/ # 订单域
│ └── product/ # 商品域
├── common/ # 通用模块
│ ├── components/ # 全局公共组件
│ ├── hooks/ # 公共自定义 Hooks
│ ├── utils/ # 通用工具函数
│ └── constants/ # 全局常量
├── assets/ # 静态资源
├── router/ # 路由配置
└── App.vue
分层原则:
- 视图层(views / components):只负责 UI 展示与用户交互,不直接调用接口。
- 业务逻辑层(hooks / store):通过自定义 Hooks 和 Pinia Store 封装业务逻辑。
- 数据层(api):统一管理接口调用、请求封装、数据转换。
- 公共层(common):跨域共享的工具、组件、类型定义。
这种拆分让多人协作时很少产生文件冲突,修改一个业务域的功能基本不跨出该域目录。
公共组件库与业务组件库沉淀
区别清楚两类组件:
- 公共组件:与业务无关,只用 Props 接收数据,如
BaseButton、BaseTable、BaseModal。它们应当无副作用、高可复用,沉淀至common/components/,甚至抽成独立的 npm 包供多个项目共享。 - 业务组件:包含特定业务逻辑,如
UserSelect(用户选择器,内置搜索用户接口)、OrderStatusTag(订单状态标签,自动映射状态码到颜色)。它们放在各自业务域的components/下,由域内页面组装。
组件库的维护规范:
- 每个公共组件至少提供 Props 表格、Slots 说明、示例代码。
- 使用 Storybook 或 Vue Styleguidist 做组件文档,降低跨团队沟通成本。
- 业务组件只暴露必要的 Props/Events,不把内部逻辑泄露给父组件。
权限体系搭建
大型项目的权限通常包含路由权限和功能权限两个维度。
路由权限:后端返回该用户可访问的路由表,前端动态添加。
// router/index.js
import { createRouter } from 'vue-router'
import { filterAsyncRoutes } from './permission'
// 基础公开路由
const publicRoutes = [ /* ... */ ]
const router = createRouter({
routes: publicRoutes
})
// 用户登录后,获取异步路由并动态添加
export async function loadDynamicRoutes(permissionList) {
const asyncRoutes = filterAsyncRoutes(allAsyncRoutes, permissionList)
asyncRoutes.forEach(route => router.addRoute(route))
}
Vue Router 的 addRoute 方法可以实现运行时注入路由,配合路由守卫判断用户是否已加载动态路由。
功能权限:通过自定义指令或 Hooks 控制按钮/区块的可见性。
<template>
<button v-permission="'user:delete'">删除用户</button>
</template>
自定义指令内部从 Pinia 读取当前用户的权限列表,若无权限则移除元素或置为不可用。
埋点体系
埋点不应该散落在各个组件的 onClick 里,而需要声明式、无侵入。
方案:自定义指令 v-track,在需要埋点的元素上声明事件类型和参数。
<button v-track.click="{ event: 'submit_order', extra: { orderId } }">提交订单</button>
指令内部在 mounted 时绑定事件,调用统一的埋点上报函数:
app.directive('track', {
mounted(el, binding) {
const { event, extra } = binding.value
el.addEventListener('click', () => {
analytics.send(event, extra) // 对接神策/GA/自研平台
})
}
})
对于页面浏览、曝光等事件,可在路由守卫或自定义 Hooks 中集中处理。关键是要把埋点逻辑从业务代码中抽离,变更埋点方案时只需改一处。
错误监控体系
错误分为两类:JS 运行时错误和接口/业务错误。
- 全局 JS 错误:在 Vue 应用初始化时设置
app.config.errorHandler,捕获所有未被 try-catch 处理的组件渲染、侦听器、生命周期错误,统一上报。
app.config.errorHandler = (err, instance, info) => {
// 上报到 Sentry / 自研平台
errorReporter.captureException(err, { instance, info })
}
- 接口错误:在 Axios 响应拦截器中统一处理 4xx、5xx 以及网络错误,按错误码做不同策略(如 Token 过期跳登录、其他错误展示全局通知),同时上报异常详情(接口路径、入参、错误信息)。
- 业务错误:对于需要人工关注的关键业务失败(如下单失败但页面未崩溃),通过封装一个
reportBusinessError函数在 catch 块中主动调用。
此外,可以结合 Sentry 的 SDK 细粒度追踪组件错误,配合面包屑和用户行为回溯,让生产环境的问题定位不再是盲猜。
最后的原则
大型 Vue 项目架构没有银弹,但有一条铁律:每个模块都应该能被独立理解、独立测试、独立替换。如果你的工程师需要同时读四个文件夹的文件才明白一个页面在做什么,那说明架构分层还不够清晰。不断收敛模块间的耦合点,用明确的接口(Props、Emits、Hooks 返回值)进行通信,才能让项目在体量膨胀时仍保持可控。