5.3 最佳实践与设计模式


文档摘要

5.3 最佳实践与设计模式 设计模式在 Angular 语境里不是四人帮图谱的复读,而是把第 3 章的机制契约翻译成日常代码习惯:容器与展示分离划定"谁能改状态",哑组件加 OnPush 把检查范围钉死在最小单元,守卫把权限挡在组件树构建之前,仓储把数据访问压进一层,状态机服务消灭散落的布尔旗子。本节每个模式都标注它保护的是哪条主线契约——模式不是装饰,是机制的日常执行手段。 先定位,再出发 5.1 定了三层骨架,5.2 画了库边界,本节处理边界内部的"细胞级"结构:单个页面、单个组件、单个服务该怎么写。读完本节你应当能够: 用容器展示分离重写一个典型页面,说出两类组件各自的变更检测策略。 给哑组件配 OnPush 并保证输入不可变,解释为什么它"想被检查都难"。

5.3 最佳实践与设计模式

设计模式在 Angular 语境里不是四人帮图谱的复读,而是把第 3 章的机制契约翻译成日常代码习惯:容器与展示分离划定"谁能改状态",哑组件加 OnPush 把检查范围钉死在最小单元,守卫把权限挡在组件树构建之前,仓储把数据访问压进一层,状态机服务消灭散落的布尔旗子。本节每个模式都标注它保护的是哪条主线契约——模式不是装饰,是机制的日常执行手段。

先定位,再出发

5.1 定了三层骨架,5.2 画了库边界,本节处理边界内部的"细胞级"结构:单个页面、单个组件、单个服务该怎么写。读完本节你应当能够:

  1. 用容器展示分离重写一个典型页面,说出两类组件各自的变更检测策略。
  2. 给哑组件配 OnPush 并保证输入不可变,解释为什么它"想被检查都难"。
  3. 用路由守卫把鉴权挡在激活之前,用仓储隔离数据访问层。
  4. 把散布的布尔状态收拢成状态机式服务,并说明它与信号路线的配合。

一、容器与展示:把"从哪来"与"长什么样"分开

一个订单列表页,两类职责常被搅在一起:取数、分页、筛选、选中逻辑是"从哪来";列表骨架、空态、加载占位是"长什么样"。容器展示分离(smart/dumb)就是把它们拆成两种组件:

// 容器组件:负责取数与状态,不写界面细节 @Component({ selector: 'app-order-page', imports: [OrderListComponent], changeDetection: ChangeDetectionStrategy.OnPush, // 容器同样默认 OnPush template: `<app-order-list [orders]="orders()" [loading]="loading()"/>` }) export class OrderPageComponent { private api = inject(OrderRepository); // 5.1 的仓储,不是直接注 HttpClient orders = signal<Order[]>([]); loading = signal(true); constructor() { // 数据一到,信号写入即调度检查——容器是数据进入组件树的闸口 effect(() => this.orders.set(this.api.orders())); } }
// 展示组件(哑组件):只有输入输出,不知道数据从哪来 @Component({ selector: 'app-order-list', changeDetection: ChangeDetectionStrategy.OnPush, // 哑组件必配 OnPush template: ` @for (o of orders(); track o.id) { // track 复用:2.5 节的结论 <div class="row" (click)="selected.emit(o.id)">{{ o.no }} — {{ o.amount }}</div> } @empty { <p class="empty">暂无订单</p> } ` }) export class OrderListComponent { orders = input.required<Order[]>(); // 输入只进不改 selected = output<number>(); // 唯一的"出口"是事件 }

分离的红利直接落在主线上:哑组件没有状态可变,OnPush 下父组件未输入新引用它就被整棵跳过;容器集中在少数几个页面级节点,检查的"入口"一眼可数。反例也常见——哑组件里偷偷注入服务刷新自己,OnPush 立刻名存实亡。

二、守卫与仓储:边界上的两个把关人

守卫把"谁能进"挡在路由激活之前,仓储把"数据怎么来"压在一层。两者都是 5.1 分层架构的执行器:

// 守卫:在组件树构建之前完成鉴权,未授权的组件根本不会被创建 export const authGuard: CanActivateFn = () => { const auth = inject(AuthService); const router = inject(Router); return auth.isLoggedIn() ? true : router.parseUrl('/login'); }; // 仓储:表现层只见类型与方法,不见 HttpClient 细节 @Injectable({ providedIn: 'root' }) export class OrderRepository { private http = inject(HttpClient); private cache = new Map<string, Order[]>(); // 请求级缓存,防止重复拉取 list(status: OrderStatus): Signal<Order[]> { // 返回信号,调用方直接接模板 const key = String(status); if (!this.cache.has(key)) { this.http.get<Order[]>('/api/orders', { params: { status: key } }) .subscribe(list => this.cache.set(key, list)); } return toSignal(fromCache(this.cache, key), { initialValue: [] as Order[] }); } }

守卫的价值在主线视角很具体:被拦截的路由根本不进组件树,省掉的不只是渲染,还有一轮无意义的检查;仓储的价值在于缓存命中时连 HTTP 都不发——性能最好的请求和最好的检查,都是没发生的那次

三、状态机式服务:消灭布尔旗子

isLoadingisErrorisEmpty 三个布尔能拼出五种非法组合,if 嵌套随之滋生。状态机的做法是承认"页面在同一时刻只处于一种状态":

type PageState<T> = | { phase: 'idle' } | { phase: 'loading' } | { phase: 'error'; retry: () => void } // 错误带重试动作,模板不用猜 | { phase: 'done'; data: T }; @Injectable({ providedIn: 'root' }) export class OrderListState { private _state = signal<PageState<Order[]>>({ phase: 'idle' }); readonly state = this._state.asReadonly(); // 对外只读信号,改状态只有一条路 load(repo: OrderRepository, status: OrderStatus) { this._state.set({ phase: 'loading' }); repo.list(status).subscribe?.(() => {}); // 仓储内部完成信号写入 } fail(retry: () => void) { this._state.set({ phase: 'error', retry }); } }

模板里用 @switch (state().phase) 分派四种视图,非法组合在类型层面就不存在。这与 2.9 节的信号路线天然咬合:状态是只读信号,检查只发生在状态迁移的那一次写入之后。

图:五个模式各自守住的主线契约

图:五个模式各自守住的主线契约

四、判例:同一个页面的三种写法

背景:工单列表页,三个时期的代码并存——A 版单组件全塞(取数、分页、弹窗、导出两千行);B 版拆了哑组件但输入传的是可变对象,OnPush 形同虚设;C 版按本节五个模式重写。
操作:C 版一个容器、三个哑组件、一个仓储、一个状态机服务;守卫拦下无权限访问。
结果:主列表交互的检查范围从整页缩到行组件;无权限请求从"拉完数据再判"变成"根本不拉";两千行组件拆成六个两百行以内的单元,新人半天上手。
解读:三个版本功能相同,差别全在"把机制契约交给谁执行"——A 版交给运气,B 版交给注释,C 版交给结构。
变式:表单密集的页面可以把状态机换成 2.7 节的响应式表单 FormGroup,两者都满足"单一事实来源";选哪个看校验逻辑与异步联动的多寡。

本节要点回顾

  • 容器展示分离:容器管数据与状态,哑组件只收输入发事件,检查入口收敛到页面级。
  • 哑组件必配 OnPush:配合不可变输入与 track,跳过检查是常态而非优化。
  • 守卫与仓储:一个挡住不该进组件树的访问,一个挡住不该发生的请求。
  • 状态机服务:用可区分联合消灭布尔组合,只读信号让每次迁移恰好触发一次检查。
  • 主线上的一环:模式是机制的执行手段,选型的唯一标准是它守住哪条契约。

下一节把这些模式放进一个完整项目,从第一次点击走到生产部署。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U