当 Vue 项目从一个“个人练手”或“小型活动页”逐渐发展成 几十个页面、多个业务团队协作、功能持续迭代 的大型应用时,单纯靠“按功能划分目录”已经不够了。这时候需要引入业务域拆分与模块分层的架构思想,目的是让代码组织方式匹配业务复杂度,降低耦合,提升可维护性。
业务域拆分:按“业务领域”划地盘
传统的目录结构通常是“按技术角色”分层:
src/
├── components/ # 所有组件混在一起
├── views/ # 所有页面
├── store/ # 所有状态
├── api/ # 所有接口
└── utils/ # 所有工具函数
当业务模块超过 5 个时,一个修改就可能散落在 views/Order.vue、store/order.js、api/order.js、components/OrderDetail.vue 等四五个目录中,跳转查找低效,且多人同时修改时极易产生冲突。
业务域拆分的思路是:按业务领域(如用户、订单、商品)将相关代码聚合在一起,每个域内自行管理它的组件、状态、接口、工具等。
src/
├── domains/
│ ├── user/ # 用户域
│ │ ├── components/
│ │ │ └── UserProfile.vue
│ │ ├── store/
│ │ │ └── userStore.ts
│ │ ├── api/
│ │ │ └── userApi.ts
│ │ └── utils/
│ │ └── formatUser.ts
│ ├── order/ # 订单域
│ │ ├── components/
│ │ │ ├── OrderList.vue
│ │ │ └── OrderDetail.vue
│ │ ├── store/
│ │ │ └── orderStore.ts
│ │ ├── api/
│ │ │ └── orderApi.ts
│ │ └── views/
│ │ └── OrderPage.vue
│ └── product/ # 商品域
│ └── ...
├── shared/ # 跨域共享的通用模块
│ ├── components/ # 全局通用组件(按钮、弹窗等)
│ ├── utils/ # 通用工具函数(日期格式化、防抖等)
│ └── api/ # 基础请求封装
└── app/ # 应用级配置(路由、全局样式、入口)
├── router/
└── App.vue
拆分原则:
- 高内聚:一个业务域内的代码尽量自给自足,域内修改不影响其他域。
- 低耦合:域与域之间通过明确的接口(如 Pinia Store 的 public getters/actions、共享组件 Props)通信,避免直接引用域内部实现细节。
- 按团队边界拆分:如果多个业务由不同团队负责,业务域可以进一步拆成独立的 Git 子仓库或 Monorepo 包,实现物理隔离。
实际收益:
- 新成员接手“订单模块”时,只需要关注
domains/order/下的代码,不会被其他几十个文件干扰。 - 订单功能的迭代、重构甚至下线,影响范围被严格限定在域内,降低“牵一发而动全身”的风险。
- 如果项目足够大,甚至可以单独为某个域做性能分析、测试覆盖,管理颗粒度更细。
模块分层:在业务域内部分层
即使做了业务域拆分,某个域内部的一坨代码仍然可能难以维护。比如 OrderDetail.vue 里同时塞了 API 请求、数据转换、业务判断、DOM 事件处理……这种“面条代码”需要进一步按职责分层。
在 Vue 项目中,一个比较实用的分层模型是 视图层 → 逻辑层 → 数据层。
1. 视图层(View/Component)
负责渲染 UI 和接收用户交互,保持“纯净”:
- 只关心 DOM 结构、样式、事件绑定。
- 不直接写业务判断(如
if(user.role === 'admin')),而是通过逻辑层暴露的状态和事件。 - 从逻辑层获取数据,把用户操作传递给逻辑层。
<!-- views/OrderPage.vue - 视图层 -->
<template>
<div>
<OrderList
:orders="orderList"
@select="handleSelect"
/>
<OrderDetail
v-if="selectedOrder"
:order="selectedOrder"
@cancel="cancelOrder"
/>
</div>
</template>
<script setup lang="ts">
import { useOrderList } from '../composables/useOrderList'
import { useOrderAction } from '../composables/useOrderAction'
const { orderList, selectedOrder, loadOrders } = useOrderList()
const { cancelOrder } = useOrderAction()
const handleSelect = (order: Order) => {
selectedOrder.value = order
}
onMounted(() => loadOrders())
</script>
2. 逻辑层(Composable / Hooks)
这一层是 Vue 应用的核心,负责全部业务逻辑。全部用组合式 API 的自定义 hooks 实现:
- 数据获取、状态管理。
- 表单校验规则、状态机流转。
- 与 Pinia Store 或 API 层交互。
- 对外暴露响应式状态和方法,供视图层消费。
// composables/useOrderList.ts
import { ref } from 'vue'
import { orderApi } from '../api/orderApi'
export function useOrderList() {
const orderList = ref<Order[]>([])
const selectedOrder = ref<Order | null>(null)
async function loadOrders(params?: any) {
orderList.value = await orderApi.fetchList(params)
}
// 可暴露更多计算属性、watch 等
return { orderList, selectedOrder, loadOrders }
}
3. 数据层(API + Store)
负责与外部数据源(后端接口、localStorage、WebSocket)打交道:
- 纯数据访问函数,不包含任何视图相关逻辑。
- Pinia Store 可以看作数据层的“缓存 + 共享状态”层,它本身也可以被逻辑层调用。
// api/orderApi.ts
import request from '@/shared/api/request'
export const orderApi = {
fetchList: (params: any) => request.get('/api/orders', { params }),
cancel: (id: string) => request.post(`/api/orders/${id}/cancel`)
}
这样分层后,一个典型的数据流是:
视图触发事件 → 逻辑层方法执行 → 调用数据层接口 →
逻辑层更新响应式状态 → 视图自动更新
分层的实际好处:
- 可测试性强:逻辑层不依赖 DOM,可以用 Vitest 独立测试组合式函数。
- 可复用:同一个
useOrderList可能在订单列表页、首页订单卡片、后台管理页同时复用。 - 可维护:需要换接口时只改数据层,需要换 UI 框架时只改视图层,核心业务逻辑不动。
- 新人友好:逻辑清晰,看一层关注一层,不用在 800 行的组件文件中反复横跳。
总结:一张图看清架构
┌─────────────────────────────────────────┐
│ 业务域 A / B / C │
│ │
│ 视图层 (Vue 组件) │
│ ↓↑ 事件/状态 │
│ 逻辑层 (Composables / Hooks) │
│ ↓↑ 数据/动作 │
│ 数据层 (Pinia Store / API / 工具) │
│ │
├─────────────────────────────────────────┤
│ 共享模块 (shared) │
│ 通用组件 / 工具 / 基础请求 │
└─────────────────────────────────────────┘
对于大多数中型项目(10-50 个页面),可以先按业务域划分目录,每个域内部再简单分层。对于超过 100 个页面、多团队并行的大项目,引入业务域拆分 + 模块分层能显著降低维护成本,让前端架构像后端微服务一样,做到“高内聚、低耦合、职责清晰”。