人人都会AI编程

19.4 大型项目分层架构规范:视图层、业务层、数据层、公共层

更新时间:2026-07-09

当项目代码量突破数万行,参与开发的同事超过三五个人时,最危险的信号是“我该把这个逻辑写在哪儿”没有确定答案。组件里开始出现 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.tsorder.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 个页面时,把这些代码按层级收拢就是值得的。分层的时机不是项目启动的第一天,而是你第一次感到“这东西我改过三遍了”的那个下午。