抽象服务(AbstractService)
AbstractService 是前端框架中最核心的能力之一:它在保持 request 方法签名简单 的同时,内置了:
- HTTP 基础能力封装(基于
Url枚举与ResultVO) - ECC 密钥交换与报文加解密
- 防重复提交(时间窗口去重)
- 设备标识采集(设备 ID + 硬件指纹签名)
- 请求头自动管理与重试机制
1. 设计目标
传统方式中,一个普通 Service 往往长成这样:
// 传统写法(示例)
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
listUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users', {
headers: {
Authorization: localStorage.getItem('token') ?? '',
},
});
}
}
问题在于:
- 每个 Service 都要关心 baseUrl、路径、请求头、错误处理
- 难以统一做 设备指纹、防重复提交、加解密 等横切逻辑
AbstractService 的目标是:把这些基础能力全部下沉,让业务 Service 只关心「调用哪个 Url、传什么参数」。
2. 基本用法
import {Injectable} from '@angular/core';
import {AbstractService, ResultVO, Url} from '@rydeen/angular-framework';
import {AppUrl} from '../../core/constants/app.url';
import {User} from '../types/users.types';
@Injectable({ providedIn: 'root' })
export class UserService extends AbstractService {
async list(): Promise<ResultVO<User[]>> {
return await this.request<User[]>(AppUrl.Users.List);
}
async save(user: User): Promise<ResultVO<void>> {
return await this.request<void>(AppUrl.Users.Save, user);
}
}
AppUrl.Users.List/Save是一个Url枚举实例,内部已经包含了 HTTP 方法与路径。
3. 请求流程(Mermaid)
你只在顶层看到一个 request<T>(url, body),但框架在内部完成了一整套安全与稳定性逻辑。
4. 防重复提交与重试
DuplicateSubmitModule 使用「时间窗口 + 请求内容哈希」的方式生成请求 ID:
- 在同一时间窗口内,相同 URL + 方法 + 请求体 + 用户 的请求仅允许通过一次
- 可以有效避免按钮连点导致的重复扣款 / 重复创建订单
如果需要开启重试机制,可以在创建 AbstractService 子类时覆盖配置(示意):
// 示例:在构造函数或工厂中传入 HttpClientConfig(具体实现见源码)
const config: HttpClientConfig = {
baseUrl: environment.apiBase,
maxRetries: 2,
retryInterval: 500,
duplicateSubmitTimeWindow: 3000,
};
5. 设备标识(设备 ID 与硬件指纹)
通过 AbstractService 发起的请求,会在统一的 Header 构建阶段自动附带两类设备标识字段:
-
X-Device-Id:稳定的浏览器设备 ID- 由
getOrCreateDeviceId()基于浏览器指纹生成一次,并持久化在localStorage/sessionStorage; - 用于在较长时间维度上标识同一物理终端(同一浏览器实例)。
- 由
-
X-Hardware-Fingerprint:带安全字段的硬件指纹签名- 由
generateSecureHardwareFingerprint()+signHardwareFingerprint()动态生成,包含时间戳与随机数; - 后端可用来做「可信设备」「异常登录检测」「Token 绑定设备」等安全策略。
- 由
这些字段的几个关键点:
- 对前端业务是透明的:业务代码无需关心如何生成/保存,只要正常调用
request()即可; - 主要供后端使用:用于做客户端身份标识、风控与审计;
- 前端只需要注意:不要在自定义拦截器 / Mock 层中无意删除或篡改
X-Device-Id、X-Hardware-Fingerprint等 Header。
6. 在应用中扩展:AbstractAppService
在 cross-platform-app 中,并没有直接让业务 Service 继承 AbstractService,而是通过一层 应用级抽象服务 来约束所有 Service 的行为:
// src/app/shared/abstracts/abstract.app.service.ts
import {
AbstractService,
EventManager,
RequestHeader,
ResultVO,
Url,
} from '@rydeen/angular-framework';
import { inject } from '@angular/core';
import { AppEvent } from '../../core/constants/app.event';
export abstract class AbstractAppService extends AbstractService {
private readonly eventManager = inject(EventManager);
override async request<T>(url: Url, body?: any, header?: RequestHeader): Promise<ResultVO<T>> {
this.eventManager.publish(AppEvent.SHOW_GLOBAL_LOADING, true);
return await super.request<T>(url, body, header, async () => {
this.eventManager.publish(AppEvent.SHOW_GLOBAL_LOADING, false);
});
}
}
所有具体 Service(如 UserService、MenuService 等)统一继承 AbstractAppService,自然获得:
- 全局 Loading 遮罩(开始请求时触发显示,结束时自动关闭)
- 统一的错误处理与日志策略(可继续在此处扩展)
7. 小结:对比传统写法的优势
- 统一的 HTTP 调用入口:所有接口请求都经由
AbstractService.request,方便加限流、埋点、追踪。 - 内置安全与稳定能力:ECC 加密、防重复提交、重试等开箱即用。
- 设备标识自动附带:设备 ID + 硬件指纹签名对前端透明、对后端可见,便于做安全与风控策略。
- 与后端 ResultVO / Page 完全对齐:公共类型在前后端共享,接口语义更清晰。
- 易于二次封装:应用可以像
AbstractAppService一样,根据自身需求增加 Loading / Toast / 埋点等横切逻辑。