DDD 架构设计
@hfyidu/api 内部按照 DDD(领域驱动设计,Domain-Driven Design)思想组织代码。每个业务领域(Domain)都是一个独立的目录,遵循统一的分层结构,互相之间边界清晰、依赖明确。
分层结构
domains/<domain>/
├── index.ts # 领域出口:XxxDomain 类 + 统一 re-export
├── cases/ # 用例层:XxxUseCase + XxxAggregateRoot
│ ├── xxx_use_case.ts
│ └── index.ts
├── repository.ts # 仓储层:封装 HTTP 请求细节
├── entities/ # DTO 层:响应数据模型
│ ├── xxx.dto.ts
│ └── _converts/ # DTO 与后台原始 JSON 的转换逻辑(内部实现,不对外暴露)
├── request_params/ # 请求参数类型
└── enums/ # 业务枚举一次接口调用的完整链路:
业务代码
│ UserDomain.cases.login.run(params)
▼
Domain(领域出口) ──聚合── AggregateRoot(聚合根,管理该领域下所有用例)
▼
UseCase(用例,一个用例对应一个具体接口能力)
▼
Repository(仓储,负责拼接 URL、选择 HTTP 方法、调用 httpApi、指定 DTO 格式化器)
▼
httpApi(统一请求核心:签名 / 鉴权 / Token 续期 / 错误处理)
▼
后台 HTTP 接口
▼
DTO.fromJson(将后台原始 snake_case JSON 转换为类型化、驼峰命名的 DTO 实例)Domain(领域出口)
每个领域导出一个 XxxDomain 类,继承自 BaseDomain,通常只做两件事:
- 持有该领域的聚合根实例:
static readonly cases = new XxxAggregateRoot() - re-export 该领域的 DTO / 枚举 / 请求参数类型,作为该领域对外的唯一入口
export class AdDomain extends BaseDomain {
static readonly cases = new AdAggregateRoot()
}业务代码只需要 import { AdDomain } from '@hfyidu/api/ad',通过 AdDomain.cases.xxx 访问具体能力,不需要关心内部文件结构。
AggregateRoot(聚合根)
聚合根以 getter 的形式暴露该领域下的所有用例,每次访问都会返回一个新的 UseCase 实例:
export class AdAggregateRoot {
public get list() {
return new AdListUseCase()
}
public get event() {
return new AdEventUseCase()
}
}- 对于一次性请求(如登录、提交反馈),每次调用
Domain.cases.xxx都拿到全新实例,天然无状态污染 - 对于分页列表(继承
PaginatorUseCase),因为需要持有list/hasMore等状态,业务代码需要自行缓存这个 getter 返回的实例(只取一次,赋值给变量复用),而不是每次都重新访问Domain.cases.xxx,否则分页状态会丢失。详见快速开始 · 分页列表请求。
UseCase(用例)
每个用例对应一个具体的接口能力,继承 BaseUseCase<Params, Response>:
export default class AdListUseCase extends BaseUseCase<AdListQueryParams, AdListDto[]> {
protected onRun(params: AdListQueryParams): Promise<AdListDto[]> {
return adRepository.list(params)
}
}BaseUseCase 提供统一的 run(params) 入口,内部调用子类实现的 onRun,并将结果包装为:
interface UseCaseResponse<Response> {
data: Response | null
updatedAt: number // 本次请求完成的时间戳(毫秒)
}分页列表用例额外继承 PaginatorUseCase,自动处理页码递增、list 数据累加/替换、hasMore 判定等分页状态管理,详见快速开始。
Repository(仓储)
仓储层是唯一直接调用 httpApi 的地方,负责:
- 通过
ApiPathBuilder拼接接口路径(每个仓储对应后台的一个serviceName服务前缀) - 指定 HTTP 方法(
GET/POST等)与参数位置(query/body) - 指定
formatter(通常是XxxDto.fromJson或XxxDto.toList),将后台原始 JSON 自动转换为类型化 DTO 实例
class AdRepository extends BaseRepository {
constructor() {
super(httpApi, 'adPlay')
}
async list(params: AdListQueryParams) {
return this.httpApi.request({
url: this.pathBuilder.resolve('list'),
method: HttpRequestMethod.GET,
query: params,
formatter: AdListDto.toList,
})
}
}业务代码不会直接接触 Repository,全部通过 UseCase 间接调用。
DTO(数据模型)
响应数据统一封装为 DTO 类,继承 BaseDto,通过装饰器声明字段元信息:
| 装饰器 | 作用域 | 说明 |
|---|---|---|
@Required() | 字段 | 声明字段为必填,后台未返回时会在开发期给出提示 |
@Default(value) | 字段 | 声明字段默认值;value 也可以是另一个 DTO 类,用于嵌套对象自动实例化 |
@OriginalKey('snake_case_key') | 字段 | 声明该字段对应后台原始 JSON 中的 key(后台是 snake_case,DTO 属性统一驼峰命名) |
@List | 类 | 声明该 DTO 支持数组批量转换(配套 XxxDto.toList 静态方法) |
@Freeze | 类 | 声明该 DTO 实例创建后不可变 |
@List
export class AdListDto extends BaseDto {
@Required()
public id: Identity
@OriginalKey('ad_location')
@Default([])
public adLocation: AdLocationDto[] // 嵌套 DTO,自动递归转换
static get fromJson() {
return _$AdListDtoFromJson()
}
static get toList() {
return _$AdListDtoToList
}
}每个 DTO 类都提供 fromJson(单条转换)和可选的 toList(列表批量转换)静态方法,由 Repository 层作为 formatter 传给 httpApi.request,实现"请求发出 → 后台返回原始 JSON → 自动转换为类型化 DTO"的全自动流程,业务代码拿到的永远是类型完整、字段驼峰命名的 DTO 实例,不需要手动做任何字段映射。
请求参数(Request Params)
每个用例对应一个请求参数接口(XxxQueryParams 用于 GET 查询参数,XxxBodyParams 用于 POST 请求体),字段命名与后台协议保持一致(通常为 snake_case,因为直接作为 query/body 透传给后台,不经过 DTO 转换层):
export interface AdEventBodyParams {
page: string
ad_provider_id: string
ad_type: AdShowType
// ...
}枚举(Enumeration)
业务枚举统一继承 BaseEnumeration,每个枚举值同时携带数值(value)、代码(code)、中文文案(text)三种表达:
export class AdShowType extends BaseEnumeration {
static BANNER = new AdShowType(1, 'banner', '横幅')
static INTERSTITIAL = new AdShowType(2, 'interstitial', '插屏')
// ...
}常用静态方法:
| 方法 | 说明 |
|---|---|
AdShowType.find(value) | 根据数值/代码/枚举实例反查对应枚举成员,未找到返回 null |
AdShowType.values() | 获取全部枚举成员数组 |
AdShowType.keys() | 获取全部枚举成员的静态属性名数组 |
枚举实例可直接 .toString() 得到 code,.valueOf() 得到 value,方便在模板或日志中直接使用。
HTTP 请求核心
所有领域最终都通过统一的 httpApi(@hfyidu/api/http)发起请求,核心能力:
- 请求签名:按约定算法对请求头签名(时间戳 + 随机串 + MD5),防止请求被篡改
- Token 自动续期:后台返回续期标记时自动在请求头中换取新 token,并通过
AuthBridge回写业务项目的状态管理,期间的并发请求会被排队,续期完成后统一重试 - 统一错误处理:区分 HTTP 层错误、业务错误(
HttpBusinessError)、资源未解锁等场景,非ignore请求会自动 Toast 提示 - 与业务解耦:库本身不感知具体使用哪种状态管理方案,通过
AuthBridge由业务项目注入 token 读写实现
详细初始化方式见快速开始。
已规划的业务领域
| 领域 | subpath | 说明 |
|---|---|---|
| ad | @hfyidu/api/ad | 广告:广告位列表、广告事件上报 |
| feedback | @hfyidu/api/feedback | 意见反馈:获取反馈条目列表 |
| index-domain | @hfyidu/api/index-domain | 首页/公共接口:图形验证码等 |
| movies | @hfyidu/api/movies | 短剧:分类、播放、解锁、收藏、观看历史、排行榜 |
| novel | @hfyidu/api/novel | 小说:书架、目录、阅读、解锁、搜索、排行榜 |
| order | @hfyidu/api/order | 订单/支付:商品、下单、支付结果查询、微信虚拟支付 |
| spread | @hfyidu/api/spread | 推广:推广信息 |
| system | @hfyidu/api/system | 应用配置:全局配置、Tabbar、行为上报 |
| user | @hfyidu/api/user | 用户体系:登录、绑定、消费记录、订阅、系统消息等 |
某些领域之间存在真实的业务依赖关系(例如 novel 阅读页插广告会依赖 ad 领域的类型),构建时这类跨领域依赖会被自动带上,无需手动额外引入。
构建与发布架构
- 构建工具链:Rollup 多入口构建(每个领域一个独立入口)+ esbuild 转译压缩 +
rollup-plugin-dts类型声明打包 - 产物形态:每个领域独立产出 ESM(
.mjs) + CJS(.cjs) + 类型声明(.d.ts),公共代码由 Rollup 自动提取为共享 chunk,不会重复打包 - 体积与安全:不发布 sourcemap,产物经 esbuild 压缩后再经
javascript-obfuscator混淆(标识符重命名 + 字符串编码),生产环境完全移除console调用 - 按需引入:
package.json的exports字段为每个领域声明独立 subpath,业务方import from '@hfyidu/api/<domain>'时只会打入该领域及其真实依赖的代码
下一步
查看各业务领域的完整用例列表、请求参数与响应 DTO 字段说明:从业务领域文档开始浏览。