Skip to main content

页面基类(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 想要在“所有按钮点击前后”加入埋点、权限、节流等逻辑,只需修改基类一次即可,不用逐页面打补丁。
  • 更易做批量分析与生成:

    • 通过静态分析 data-id 与 registerHandler,AI 可以自动生成「页面交互说明」「事件清单表」;
    • 也能基于这些 key 自动补全或修改交互(例如:为所有 *.saveBtn 增加统一确认弹窗)。

简单来说:
统一的事件分发模型,把“交互协议”变成了机器可读的结构化约定,这比零散的 (click)="xxx()" 更利于 AI 理解与批量操作,也为未来的 AI 全自动开发流水线(自动生成页面、自动修改交互规则等)打下了基础。