页面基类(AbstractComponent & AbstractAppPage)
在传统 Angular 项目中,页面组件通常直接继承 Component,每个页面各自处理:
- 路由跳转
- 国际化文案
- Snackbar / Dialog 交互
- 事件订阅与销毁
这些逻辑容易散落在多个组件中,既难以复用,也不利于架构治理。
本框架通过两层抽象统一页面行为:
- 框架层:
AbstractComponent—— 统一路由、导航与消息接口定义 - 应用层:
AbstractAppPage—— 集成事件总线、统一 UI 事件分发与消息提示
1. 框架层:AbstractComponent
AbstractComponent 提供了最基础的能力:
- 访问路由参数 / 查询参数
- 基于
Url枚举的导航方法 - 定义消息提示与确认的抽象方法
import { AbstractComponent, ConfirmListener, MessageOptions, MessageParam, Method } from '@rydeen/angular-framework';
import { ActivatedRoute, NavigationBehaviorOptions, Router } from '@angular/router';
import { inject } from '@angular/core';
import { Url } from './url';
export abstract class AbstractComponent {
protected router: Router = inject(Router);
protected route: ActivatedRoute = inject(ActivatedRoute);
protected getParam(key: string): string {
return this.route.snapshot.paramMap.get(key) || '';
}
protected getQueryParam(key: string): string {
return this.route.snapshot.queryParamMap.get(key) || '';
}
protected async navigateByUrl(url: Url, extras?: NavigationBehaviorOptions): Promise<boolean> {
if (url.method !== Method.NAVIGATOR) return false;
return await this.router.navigateByUrl(url.value(), extras);
}
// 由子类实现的消息接口
abstract info(message: string, options?: MessageOptions): Promise<void>;
abstract error(message: string, options?: MessageOptions): Promise<void>;
abstract warn(message: string, options?: MessageOptions): Promise<void>;
abstract success(message: string, options?: MessageOptions): Promise<void>;
abstract confirm(messageId: string, messageParams?: MessageParam, confirmListener?: ConfirmListener): Promise<void>;
}
可以看到,这一层只关心「页面基础能力是什么」,而不关心如何在 Material / Ionic 等具体 UI 框架中呈现。
2. 应用层:AbstractAppPage
在 cross-platform-app 中,所有页面都继承自 AbstractAppPage,在此基础上统一了:
- 事件总线订阅与销毁(
EventManager) - SnackBar 消息提示
- 通用 UI 事件分发(click/change/submit)与
data-id机制
// src/app/shared/abstracts/abstract.app.page.ts(节选)
@Directive()
export abstract class AbstractAppPage extends AbstractComponent implements OnDestroy {
private readonly snackBar = inject(MatSnackBar);
protected readonly dialog = inject(MatDialog);
protected readonly eventManager: EventManager = inject(EventManager);
private readonly subscriptionIds: Set<string> = new Set<string>();
ngOnDestroy(): void {
this.eventManager.unsubscribe(this.subscriptionIds);
}
protected registerEvent(event: EventNameEnum, callback: (event: any) => void) {
const id = this.eventManager.subscribe(event, callback);
this.subscriptionIds.add(id);
}
// 统一 UI 事件分发:onClick/onChange/onSubmit
public onClick(event: Event): void {
this.handleUiEvent('click', event).catch(error => console.error(error));
}
public onChange(event: unknown): void {
this.handleUiEvent('change', event).catch(error => console.error(error));
}
public onSubmit(event: unknown): void {
this.handleUiEvent('submit', event).catch(error => console.error(error));
}
// 通过 data-id 映射到处理函数
public registerHandler(key: string, handler: UiEventHandler): void {
this.uiEventHandlers.set(key, handler);
}
}
模板中的统一写法
<!-- 任意页面模板,例如 users.html -->
<button
mat-raised-button
color="primary"
data-id="users.saveBtn"
(click)="onClick($event)">
保存
</button>
<mat-form-field>
<input matInput data-id="users.keywordInput" (change)="onChange($event)">
</mat-form-field>
页面类中的事件注册
export class UsersPage extends AbstractAppPage {
constructor(private readonly userService: UserService) {
super();
}
async ngOnInit() {
this.registerHandler('users.saveBtn', async () => {
await this.saveUser();
});
this.registerHandler('users.keywordInput', async (ev) => {
await this.search(ev.payload?.value);
});
}
}
这样可以做到:
- 模板层只负责声明「这个按钮/输入框属于哪个逻辑 key」
- 组件类只关心「key → handler」的映射关系
- 任何 UI 框架(Material、Ionic、NativeScript)都可以在不改业务代码的前提下更换
3. 对比传统页面写法
总结优势
- 统一的页面基类:路由、国际化、消息提示与事件总线都集中在一个抽象层里,便于维护与演进。
- 更清晰的结构:模板只负责展示与
data-id标记,页面类只负责业务逻辑,强制关注点分离。 - 易于扩展:未来如果要在点击/提交前后统一增加埋点、权限校验、防抖,只需在
AbstractAppPage覆写preDispatch/postDispatch即可。
4. 对 AI 全自动开发的优势
当我们希望让 AI Agent 自动生成/重构页面代码时,AbstractComponent / AbstractAppPage 的事件模型相比传统的「每个按钮单独绑定事件」有几个明显好处:
4.1 传统写法的问题
传统页面通常是这样写事件的:
<button mat-raised-button color="primary" (click)="onSaveUser()">保存</button>
<button mat-button (click)="onDeleteUser(user)">删除</button>
<input matInput (keyup)="onKeywordChange($event)" />
onSaveUser() { /* ... */ }
onDeleteUser(user: User) { /* ... */ }
onKeywordChange(event: KeyboardEvent) { /* ... */ }
这类写法在 AI 自动开发场景中的问题:
- 每个页面都有自己的命名习惯(
save/onSave/submit等),AI 很难从命名中归纳出统一模式,只能逐文件学习。 - 事件处理函数与模板结构紧耦合,一旦重构布局或拆分组件,需要成对修改模板和 TS 代码,自动化改动容易遗漏。
- 很难在所有页面的
click/change/submit之前/之后统一注入逻辑(权限、埋点、防抖、错误包装等),AI 也难以“全局插入”这些能力。
4.2 当前统一事件模型的好处
在本框架中,页面事件统一走 onClick/onChange/onSubmit + data-id + registerHandler 的模式,这对 AI Agent 更友好:
-
模式高度统一:
- 模板层永远是
(click)="onClick($event)"+data-id="users.saveBtn"这种固定写法; - TS 层永远通过
registerHandler('users.saveBtn', handler)注册逻辑。 - AI 只要学会这一套,就可以在任意页面按同样模式新增/修改事件,而不用猜每个页面的命名习惯。
- 模板层永远是
-
职责边界清晰,便于自动重构:
- 模板负责「元素 → key」;页面类负责「key → 处理函数」。
- 当需要拆分组件或改布局时,AI 只需保持
data-id不变,就能安全地移动节点,而无需改动事件逻辑本身。
-
便于自动注入横切逻辑:
- 所有 UI 事件最终都会经过
handleUiEvent/preDispatch/postDispatch这几个固定钩子。 - AI 想要在“所有按钮点击前后”加入埋点、权限、节流等逻辑,只需修改基类一次即可,不用逐页面打补丁。
- 所有 UI 事件最终都会经过
-
更易做批量分析与生成:
- 通过静态分析
data-id与registerHandler,AI 可以自动生成「页面交互说明」「事件清单表」; - 也能基于这些 key 自动补全或修改交互(例如:为所有
*.saveBtn增加统一确认弹窗)。
- 通过静态分析
简单来说:
统一的事件分发模型,把“交互协议”变成了机器可读的结构化约定,这比零散的 (click)="xxx()" 更利于 AI 理解与批量操作,也为未来的 AI 全自动开发流水线(自动生成页面、自动修改交互规则等)打下了基础。