当项目代码量突破数万行,参与开发的同事超过三五个人时,最危险的信号是“我该把这个逻辑写在哪儿”没有确定答案。组件里开始出现 API 请求、状态管理、工具函数调用混在一起;一个页面文件 500 行,改个样式都要翻半天。分层架构的目的不是创造条条框框,而是给团队一个默认正确的代码归属地,让每个人都能快速定位、放心修改。
一个成熟的 Vue 项目通常会形成四层结构:
视图层(View Layer)
职责:只负责“界面长什么样”和“用户交互如何触发”,不处理业务逻辑,不直接请求数据。
典型目录:
src/views/ # 页面级组件
src/components/ # 通用/业务组件
src/layouts/ # 布局组件
规范要点:
- 组件内只做三件事:声明 Props、渲染模板、触发事件或调用业务层暴露的方法。
- 不要直接在视图层写 API 调用(
axios.get(...))、复杂的数据转换逻辑或状态操作。这些应该委托给业务层或数据层。 views/下的页面组件是路由的入口,负责组装各部分;components/下的组件是纯 UI 或轻量业务组件,尽量保持无副作用。
一个好视图层的标志:你换一套 UI 框架,只需要改视图层的模板和样式,业务逻辑原封不动。
业务层(Business Layer)
职责:封装与“具体业务”相关的逻辑流程,是视图层和数据层之间的协调者。它知道“用户点击提交订单后,要先校验表单、再调用下单接口、再更新购物车状态、最后弹窗提示成功”,但不知道数据最终存在 Pinia 还是本地缓存,也不知道视图是用 Element Plus 还是 Ant Design。
典型目录:
src/composables/ # 组合式函数(Hooks)
src/services/ # 业务服务模块
规范要点:
- 通过自定义 Hook 封装可复用的业务逻辑。例如
useOrderSubmit()可以处理下单全流程,视图层只需调用submit()并监听返回状态。 - 当多个页面共享同一套业务流程时,业务层避免了每个页面都复制粘贴一遍逻辑。
- 业务层可以调用数据层的 API 方法和状态仓库,但不直接操作 DOM 或引用组件实例。
一个实用例子:
// src/composables/useOrderSubmit.ts
export function useOrderSubmit() {
const orderStore = useOrderStore()
const submitting = ref(false)
async function submit(orderData) {
submitting.value = true
try {
await orderApi.create(orderData) // 调用数据层接口
orderStore.clearCart() // 更新状态
ElMessage.success('下单成功')
} finally {
submitting.value = false
}
}
return { submit, submitting }
}
视图层只管调用 submit,业务流程的细节全部封装在内。
数据层(Data Layer)
职责:管理应用的“全局状态”和“服务端数据交互”,提供统一的数据读写接口。数据层不关心数据怎么展示,也不关心业务规则,它只回答“数据在哪、怎么存取”。
典型目录:
src/stores/ # Pinia 状态仓库
src/api/ # 接口封装
规范要点:
- 状态管理(Pinia):存放全局共享的状态,如用户信息、权限配置、购物车数据。页面私有的表单数据不要放进全局 Store,应放在组件本地或业务层 Hook 中。
- API 封装(
src/api/):所有 HTTP 请求的定义和配置集中管理,按业务模块拆分文件(如user.ts、order.ts)。视图层和业务层不应该直接import axios,而是通过 API 模块调用。 - 数据格式转换(如日期格式化、金额单位转换)统一在数据层或业务层处理,不要让视图层散落各种
formatDate调用。
接口封装示例:
// src/api/user.ts
import request from '@/utils/request'
export function getUserInfo() {
return request.get('/api/user/info')
}
公共层(Common Layer)
职责:提供与具体业务无关的、全项目公用的基础能力,是其他所有层的“工具库”。
典型目录:
src/utils/ # 工具函数(格式化、校验、加解密)
src/directives/ # 自定义指令
src/styles/ # 全局样式、变量、混合
src/assets/ # 静态资源
src/plugins/ # 第三方库注册(如 Element Plus 全局注册)
规范要点:
- 纯粹性:公共层的函数和组件必须无业务语义。
formatDate可以,formatOrderDate就应该放到业务层。 - 可测试性:工具函数保持纯函数特性,方便编写单元测试。
- 不要反向依赖:公共层绝不能引用视图层、业务层或数据层的任何东西,它是被所有层单向依赖的基石。
层级间的通信规则
为了避免日后项目变成“蜘蛛网依赖”,四层之间遵循单向调用:
视图层 → 业务层 → 数据层 → 公共层
↑ ↑ ↑
都可以引用公共层,但谁也不能往上或同级跨层引用
- 视图层可以调用业务层 Hook 和数据层 Store(轻量场景下),但不应直接调 API。
- 业务层是调度中心:调用 API、读写 Store、使用工具函数,最后将结果返回给视图层。
- 数据层只对业务层和少数视图层暴露接口,不依赖业务层。
这套分层不是教条。当一个页面只有两张表单和三个接口时,严格的四层反而是过度设计。但当你发现同一个接口在 20 个组件里被裸调,或者一个购物车清空逻辑散落在 5 个页面时,把这些代码按层级收拢就是值得的。分层的时机不是项目启动的第一天,而是你第一次感到“这东西我改过三遍了”的那个下午。