Skip to main content

模块划分与分层结构

本章节从「零面向对象经验的前端同学」视角出发,讲清楚:

  • 为什么要划分 core / shared / features 这些层级?
  • 这些目录分别放什么?互相之间能不能相互引用?
  • 如果你习惯了 Vue3 src/views + src/components + src/api 的写法,对应关系是什么?

1. 整体分层一眼看懂​

先看一个简化版的架构图(基于 cross-platform-app 实际项目):

可以简单理解为:

  • Core:城市的「基础设施」——只有一份,自来水、电力、道路。
  • Shared:多个小区之间共享的「公共配套」——公园、学校、医院。
  • Features:每个小区的「具体楼栋」——真正住人的地方,各自有自己的物业、房号。

规则只有一条:楼栋可以使用公园和自来水,但反过来不行。也就是:features 可以依赖 shared/core,但 core 绝不能依赖某个业务模块。

1.1 与传统 Vue3 项目对比​

概念本架构中的位置典型 Vue3 写法中的位置
路由配置 / 全局服务app/coresrc/router、src/store、src/api 等
通用 UI 组件app/shared/componentssrc/components 或 src/components/common
页面级组件app/features/*/*.tssrc/views
业务模块内的 serviceapp/features/*/servicessrc/api 或某些 src/modules/*/service.ts
抽象基类app/shared/abstractsVue 里通常用「mixin/composable」替代

不同之处在于:我们把“长成架构”的那部分逻辑显式抽出来,放进 core 与 shared,而不是全部堆在 src 根目录下。

2. 目录结构拆解(结合实际代码)​

下面这段来自 cross-platform-app 的 README,你在工程里能看到一模一样的目录:

src/
├── app/
│ ├── core/
│ ├── features/
│ ├── shared/
│ ├── app.config.ts
│ ├── app.routes.ts
│ └── app.ts / app.html / app.scss
└── ...

2.1 app/core —— 应用「大脑与中枢」​

核心原则:只初始化一次,全局唯一,不直接被业务模块导入 Module。

典型内容(节选):

  • animations/route-animations.ts:全局路由动画。
  • constants/app.event.ts:全局事件枚举,比如 SHOW_GLOBAL_LOADING。
  • constants/app.url.ts:所有页面路由与 API 地址集中管理。
  • db/*:数据库 schema、查询构建器。
  • services/*:扫码、数据库、迁移、语言切换等单例服务。

与 Java 世界里的 core / infrastructure 模块非常类似:不直接写业务,但所有业务都要用到它。

2.2 app/features —— 真正的业务模块​

每个子目录就是一个完整的业务域,例如:

  • features/home:首页模块(菜单搜索、小组件总览)
  • features/menu:菜单模块
  • features/users:用户模块

每个模块内部有自己的:

  • components/:该模块只在本模块使用的组件
  • services/:该模块的业务服务与数据访问
  • types/:只在本模块使用的类型定义
  • constants/:与本模块相关的配置、枚举、文案 key 等常量

约定:「先放 features,自用;真有第二个模块要复用,再提升到 shared」。这样可以避免一开始就把一切都塞进 shared,导致共享层变成「垃圾场」。

2.2.1 各目录里应该写什么代码?​

  • 组件(components/)

    • 只负责「长什么样」和「对外暴露什么事件」,不直接写业务流程。
    • 尽量让组件的输入/输出用 @Input/@Output 或事件回调表达,而不是在组件内部偷偷调用一堆 Service。
    • 示例:features/users/components/user-dialog.ts 只关心表单字段和校验,保存按钮点下去后,调用父页面传进来的 onSave(user)。
  • 服务(services/)

    • 把和这个业务模块相关的 数据获取、写入、本地缓存、状态组装 都放在这里。
    • Service 不关心具体 UI,只提供「方法 + 类型」:
      • 如 UserService.list()、UserService.save(user)。
    • 页面组件调用 Service,而不是每个组件各自去调 HttpClient/Database。
  • 类型(types/)

    • 统一描述该模块内部使用的数据结构(请求参数、响应结果、UI 状态等)。
    • 好处:当后端字段调整时,只需要改一处 type,就能通过 TypeScript 把所有受影响位置标出来。
  • 常量(constants/)

    • 放和模块强相关的枚举、配置、文案 key:例如按钮 data-id、表头配置、筛选条件 key。
    • 统一命名,避免在代码里散落大量 magic string。

2.2.2 利用「单一职责」避免代码混乱​

可以对比两种常见写法:

反例:什么都写在组件里

// users.ts(反例)
export class UsersPage {
users: User[] = [];

async ngOnInit() {
const res = await this.http.get('/api/users'); // 直接拼 URL
this.users = res.data;
localStorage.setItem('users', JSON.stringify(this.users)); // 直接操作存储
}
}

问题:

  • 组件既管 UI,又管数据获取、URL、存储,后续很难复用或测试。

推荐:页面只调 Service,Service 再调框架

// features/users/services/user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService extends AbstractAppService {
async list(): Promise<ResultVO<User[]>> {
return await this.request<User[]>(AppUrl.Users.List);
}
}

// features/users/users.ts
export class UsersPage extends AbstractAppPage {
users: User[] = [];

constructor(private readonly userService: UserService) {
super();
}

async ngOnInit() {
const res = await this.userService.list();
this.users = res.data;
}
}

现在:

  • 页面只负责「什么时候加载、怎么展示」,不关心 URL / Header / 加解密。
  • 业务 Service 的逻辑都集中在 features/users/services,避免业务散落到组件里。

简单记法:组件只管“长相”和“交互”,Service 只管“数据和规则”,types/constants 只管“约定”。 按这个思路组织 features 目录,模块内部结构会自然清晰很多。

2.2.3 页面内各文件的职责划分(HTML / SCSS / 页面类 / Service)​

每个功能模块下的页面通常由四个文件组成:{module}.html、{module}.scss、{module}.ts 以及该模块下的 services/*.ts。各自只写一类东西,避免混在一起导致难以维护。

文件职责应该写的代码不应出现的代码
{module}.html页面模板结构、绑定、指令、事件绑定(如 (click)="onClick($event)")、data-id 等纯展示与交互结构业务判断、复杂表达式、直接调用 Service
{module}.scss样式该页面的布局、颜色、间距、响应式等所有样式业务逻辑、TypeScript
{module}.ts(页面类)视图与交互层双向绑定变量、DOM 相关操作、事件处理函数(如 onClick、onChange、registerHandler)、调用 Service 并接收结果后更新绑定变量;不包含任何业务规则与数据处理业务逻辑、网络请求、复杂计算、直接操作 HttpClient/数据库
services/*.ts业务与数据层所有业务逻辑、所有网络请求、数据组装、调用 Worker Service 并汇总结果,最后把「处理结果」返回给页面类DOM 操作、模板相关代码

一句话归纳:

  • HTML:只写「长什么样、点哪里、绑什么」。
  • SCSS:只写「怎么好看」。
  • 页面类({module}.ts):只写「有什么数据要展示、点了以后调哪个 Service、拿到结果后更新哪几个变量」;不写「怎么算、怎么请求、怎么拼数据」。
  • Service:只写「怎么算、怎么请求、怎么拼数据、怎么调 Worker」;不写「怎么展示、怎么绑 DOM」。

这样,页面类只需要知道:我有需求 → 交给 Service → Service 处理完告诉我结果是什么;至于请求发到哪、是否用 Worker 并行、内部怎么拆分,全部封装在 Service 内部。

2.2.4 Service 与 Worker Service 的协作(复杂业务拆分)​

当某个 Service 方法内部逻辑很重(例如:大量数据过滤、多源数据合并、复杂统计)时,应把可拆分的子逻辑交给 Worker Service(或 Web Worker)去并行处理,主 Service 只负责:

  1. 拆分子任务并调用 Worker Service;
  2. 等待各 Worker 返回结果;
  3. 对结果进行汇总;
  4. 将最终结果返回给页面类。

网络请求一律放在 Service 中:页面类不直接使用 HttpClient,而是调用诸如 UserService.list()、MenuService.syncFromRemote() 等方法,由 Service 内部发起请求、处理 ResultVO、错误与重试,再向页面返回「业务层结果」。

流程可以概括为:

页面类只关心「调哪个方法、拿到什么结果、更新哪些变量」;所有业务细节与并行策略都留在 Service(及 Worker Service)内部,这样既避免页面类膨胀,也便于单测与复用。

2.3 app/shared —— 跨模块的「公共配套」​

职责非常明确:能被多个模块复用,但不承载具体业务逻辑。

典型内容:

  • abstracts/abstract.app.service.ts:所有业务 Service 的抽象父类。
  • abstracts/abstract.app.page.ts:所有页面组件的抽象父类。
  • components/app-layout:统一的布局框架。
  • components/data-table、components/status-badge 等通用 UI 组件。
  • types/*:像 menu.shared.types.ts 这种已经被多个模块使用的类型。

如果你是 Vue3 开发者,可以类比为:

  • shared/abstracts ≈ 一组「强约束的 composables + 基类」,而不是随意的 useXxx。
  • shared/components ≈ 全局注册的通用组件库,但仍然在项目里可见源码。

3. 面向对象视角下如何理解「模块」​

对没有 OO 背景的前端,可以用一个简单类比:

  • Module = 一个有清晰边界的「对象集合」,对外只暴露有限的入口(页面路由、Service 方法),内部可以自由重构。

以 Users 模块为例:

从外部来看,你只需要知道:

  • 想展示用户管理页面 → 跳到 /users
  • 想进行用户 CRUD → 调用 UserService 暴露的几个方法

内部这些「小对象」怎么协作,可以在不影响外部调用方的前提下自由演进。

4. 与传统 Vue3 项目目录的对比示例​

假设你之前的 Vue3 项目大致是这样:

src/
├── api/
│ ├── user.ts
│ └── menu.ts
├── components/
│ ├── DataTable.vue
│ └── UserDialog.vue
├── views/
│ ├── Home.vue
│ ├── Menu.vue
│ └── Users.vue
└── store/
└── index.ts

迁移到当前架构的大致映射关系:

Vue3 目录新架构中的位置
views/Users.vuefeatures/users/users.ts(页面组件)
api/user.tsfeatures/users/services/user.service.ts
通用表格 DataTable.vueshared/components/data-table/data-table.ts
Vuex store部分映射到 core/services + 各自模块 services/

好处:

  • 每个业务模块在文件系统里「一眼可见」,新同事打开 features/users 就能看到:页面、服务、类型都在这里。
  • 通用能力(表格、布局、事件总线、数据库)集中在 shared/core,不用每个项目重造轮子。

5. 小结:你需要记住的三条规则​

  1. Core 只初始化一次:放全局服务、枚举、DB、动画,业务模块不要直接往里塞东西。
  2. 先 features,后 shared:先让类型/组件在模块内长大,只有真的跨模块复用时才提升到 shared。
  3. 强约束的抽象基类:页面统一继承 AbstractAppPage,Service 统一继承 AbstractAppService,这样模块间的上层结构就自然统一了。

理解了这一层之后,再去看「状态管理」与「HTTP 与 API 访问层」两篇文档,会更容易对整体架构有一张完整心智图。