5.1 项目架构设计:把数据纪律落成结构 项目架构解决代码往哪放、依赖朝哪流的问题。本节给出经过实战检验的三层架构(数据访问层 / 领域状态层 / 表现层),核心主张:第 2.9 节的数据纪律不能靠口头约定,要靠目录结构、注入令牌与类型边界强制执行——结构即约束。 承接与目标 前四章攒下的机制与工具,从这一节开始收进真实工程的骨架里,先立结构再谈编码: 画出三层架构图并说明每层的职责与禁止事项。 用 InjectionToken 隔离数据访问层,让表现层无法绕过仓储直连接口。 制定目录约定与依赖方向规则。 判断"该放哪一层"的四个典型争议场景。 一、三层架构 层间规则三条:依赖只能朝下(features 可依赖 state 与 data 的令牌,state 可依赖 data,反向禁止);
项目架构解决代码往哪放、依赖朝哪流的问题。本节给出经过实战检验的三层架构(数据访问层 / 领域状态层 / 表现层),核心主张:第 2.9 节的数据纪律不能靠口头约定,要靠目录结构、注入令牌与类型边界强制执行——结构即约束。
前四章攒下的机制与工具,从这一节开始收进真实工程的骨架里,先立结构再谈编码:
src/app/ ├── core/ 核心层:认证、拦截器、全局服务(单例) │ ├── auth.service.ts │ └── http-interceptors.ts ├── data/ 数据访问层:仓储实现(唯一允许知道接口细节的层) │ ├── order.repository.ts │ └── tokens.ts 依赖令牌:表现层只见令牌不见实现 ├── state/ 领域状态层:仓库(store)、信号、派生计算 │ ├── order.store.ts │ └── cart.store.ts ├── features/ 表现层:按业务域组织的页面与组件 │ ├── orders/ │ └── reports/ └── shared/ 共享层:哑组件、管道、通用指令(不含业务状态) └── ui/
层间规则三条:依赖只能朝下(features 可依赖 state 与 data 的令牌,state 可依赖 data,反向禁止);表现层不出现接口地址(HTTP 细节封死在 data 层);shared 无状态(有状态的服务一律上移到 state 或 core)。这三条规则防的正是主线上的头号敌人:绕过状态层直连接口导致的数据双份、原地修改导致的更新失灵。
// data/tokens.ts:定义仓储接口与令牌 import { InjectionToken } from '@angular/core'; import { Observable } from 'rxjs'; export interface OrderRepository { list(kw: string): Observable<Order[]>; save(draft: OrderDraft): Observable<Order>; } export const ORDER_REPOSITORY = new InjectionToken<OrderRepository>('订单仓储'); // data/order.repository.ts:实现(唯一知道接口细节的地方) export class HttpOrderRepository implements OrderRepository { private http = inject(HttpClient); list(kw: string) { return this.http.get<Order[]>('/api/orders', { params: { kw } }); } save(draft: OrderDraft) { return this.http.post<Order>('/api/orders', draft); } } // 应用配置中绑定 providers: [ { provide: ORDER_REPOSITORY, useClass: HttpOrderRepository } ] // state/order.store.ts:状态层消费令牌(3.3 节测试时可换成内存假实现) export class OrderStore { private repo = inject(ORDER_REPOSITORY); private _list = signal<Order[]>([]); readonly list = this._list.asReadonly(); load(kw: string) { this.repo.list(kw).subscribe(rows => this._list.set(rows)); } }
令牌隔离的直接回报在测试(3.3 节):换一个内存实现注入,状态层测试不打网络。间接回报是编译期的防绕行——表现层注入 ORDER_REPOSITORY 拿到的是接口类型,接口上没有的便捷方法想调也没有。
| 场景 | 错放 | 正放 | 判断依据 |
|---|---|---|---|
| 订单状态徽标管道 | features/orders | shared | 无状态纯展示,别的域也要用 |
| 当前用户信息 | features/profile | core | 全局会话状态 |
| 列表筛选词 | state 仓库 | 路由查询参数 | URL 状态(2.9 节分类) |
| 接口分页参数拼装 | store | data 层 | 属接口细节 |

背景:接手的旧项目没有分层:组件里直接发 HTTP、五十处接口地址散落各模板类、同一份用户数据在三个组件各存一份,界面时对时错(第 2.9 节描述过症状)。
操作:三周渐进改造——第一周建 data 与 state 层骨架,接口地址集中收编进仓储;第二周三个重复的用户数据存放点合并为一个信号仓库,组件改为订阅;第三周表现层清理,注入改走令牌。
结果:接口地址从五十处收敛到一处;"数据串了"类工单清零;之后一次后端接口整体换路径前缀,只改仓储一处上线。
解读:架构改造的收益账要这么算——前期投入换的是变更影响半径的缩小;数据纪律落成结构后,新人不需要读过本书也不会犯绕行错误,因为编译器替你拦着。
变式:若项目更大,先从最痛的一个业务域开始试点(一个域全套三层),验证约定后再横向铺开——比"全项目停业务整顿"现实得多。
下一节处理规模问题:大型应用的拆分与协作。