与传统 Angular 开发模式的对比
本节站在「有一定 Angular 经验」的视角,比较基于 @rydeen/angular-framework 的架构化写法,与常见的 面向组件 / 面向过程式 开发方式之间的差异与优势。
1. 总览对比表
| 维度 | 传统 Angular 写法 | 基于框架的架构化写法 |
|---|---|---|
| 目录结构 | 按功能或组件随意划分,规则不统一 | 明确区分 core / shared / features,与 Java 服务端分层一致 |
| HTTP 调用 | 各 Service 各自注入 HttpClient,手动拼接 URL/Headers | 统一继承 AbstractService/AbstractAppService,自动处理基址、签名、防重 |
| 返回结构 | Observable<any> / Observable<any[]> 等 | Promise<ResultVO<T>> / Page<T>,与 Java 端 ResultVO 完全对齐 |
| 页面基类 | 组件直接继承 Component | 所有页面继承 AbstractAppPage,统一导航、消息提示与事件订阅 |
| UI 事件处理 | 模板中到处是 (click)="onXXX()",方法与 DOM 紧耦合 | 通过 data-id + 统一 onClick/onChange/onSubmit 分发 |
| 状态与事件 | 多处手写 Subject/EventEmitter,缺乏统一规范 | 使用 EventManager + EventNameEnum/AppEvent 集中管理事件 |
| 并发控制与本地存储 | 临时变量 + flag 控制,逻辑分散 | 提供 LocalStorage、ReentrantLock 等通用工具 |
| 学习曲线 | 简单开始,复杂业务时需要自行搭建架构 | 初期需要理解抽象基类与 Url/ResultVO,但后续开发体验更稳定 |
2. HTTP 调用:从 HttpClient 到 AbstractService
传统写法示例
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
const token = localStorage.getItem('token');
return this.http.get<User[]>('/api/users', {
headers: { Authorization: token ?? '' },
});
}
}
问题:
- URL、Header、错误处理分散在各个 Service 中
- 与后端的
ResultVO<T>协议不一致,需要在业务层反复拆封装
基于框架的写法
@Injectable({ providedIn: 'root' })
export class UserService extends AbstractAppService {
async list(): Promise<ResultVO<User[]>> {
return await this.request<User[]>(AppUrl.Users.List);
}
}
优势:
- Service 只关心「调用哪个 Url」,其它能力(设备指纹、防重试、加解密、Loading)全部下沉到基类中。
- 类型与 Java 端使用的
ResultVO<T>、Page<T>一致,前后端认知统一。
3. 页面结构:从「组件各自为政」到「统一页面基类」
传统页面
@Component({...})
export class UsersComponent implements OnInit {
loading = false;
constructor(
private snackBar: MatSnackBar,
private router: Router,
private userService: UserService,
) {}
async load() {
this.loading = true;
try {
const data = await lastValueFrom(this.userService.getUsers());
// ...
} finally {
this.loading = false;
}
}
showSuccess(msg: string) {
this.snackBar.open(msg, undefined, { duration: 2000 });
}
}
问题:
- 每个页面重复处理 Loading / SnackBar / 导航等共性逻辑
- 日后变更交互样式(如 Loading 改成全局遮罩)时,需要逐个修改页面
基于 AbstractAppPage 的页面
@Component({...})
export class UsersPage extends AbstractAppPage {
constructor(private readonly userService: UserService) {
super();
}
async ngOnInit() {
this.registerHandler('users.queryBtn', async () => {
const result = await this.userService.list();
if (result.code === '200') {
await this.success('users.load.success', { isKey: true });
} else {
await this.error(result.msg ?? 'users.load.error');
}
});
}
}
模板中:
<button
mat-raised-button
color="primary"
data-id="users.queryBtn"
(click)="onClick($event)">
查询用户
</button>
优势:
- 页面类只处理业务逻辑与事件注册,导航/消息/事件总线在抽象基类中统一实现。
- 模板与逻辑通过
data-id解耦,后续改模板不会牵连过多 TS 代码。
4. 架构视角比较(Mermaid)
可以看到,在框架化模式下:
- HttpClient 只出现在
AbstractService这一层 - 事件与 Loading 都通过
EventManager打通页面与服务 - 业务代码的「依赖面」更窄,更容易做测试与重构
5. 适用场景建议
更适合使用本框架的场景:
- 前后端都在使用润典技术栈,希望在 API 规范、类型与错误处理上保持一致
- 中大型业务系统(如收银、点餐、运营后台等),对可维护性和统一体验有较高要求
- 有大量全局横切需求:权限、埋点、Loading、错误提示、国际化等
可以考虑保持简单写法的场景:
- Demo 级别的 PoC、小工具类应用
- 只需要少量接口、交互简单,对架构统一性要求不高
在实际项目中,你也可以按「核心功能使用框架、边缘模块保留简单写法」的方式渐进式引入。