定义aspect
如果说 trigger、checker 更像 Oak 的“底层规则”,那么 aspect 更像 Oak 的“命名业务接口”。
很多业务逻辑并不适合直接表达成某个实体上的一次 create/update/remove/select,例如:
- 需要跨多个实体聚合统计;
- 需要上传文件、下载流;
- 需要调用外部服务;
- 需要把若干次
select/operate封装成一个明确的业务动作。
这类逻辑最适合写成 aspect。
aspect 的类型定义
oak-domain/src/types/Aspect.ts 的底层接口为了容纳任意业务字典而保持宽泛,但应用层不应照搬这个宽泛签名。项目应在 AspectDict.ts 中为每个方法声明具体参数和返回值,例如:
export type AspectDict = {
getLicenseSecretKey: (
params: { licenseId: string },
context: BackendRuntimeContext
) => Promise<string>;
};
也就是说,一个 aspect 本质上就是:
- 一个名字;
- 一个具有明确参数、context 和返回结果的异步函数。
编写位置
通常你会在 src/aspects 下面写两类文件:
AspectDict.ts:定义 aspect 的类型签名,方便前后端获得类型提示;index.ts:真正导出具体实现。
AspectDict.ts 中的声明只提供 TypeScript 调用合同,并不会注册运行时实现。每个需要从前端调用的名称,都必须同时出现在 src/aspects/index.ts 的默认导出对象中;否则代码可以通过类型检查,但服务端的 aspect 分发找不到该实现。
一个实现例子
const aspectDict: AspectDict = {
getLicenseSecretKey: async (params: { licenseId: string }, context) => {
const licenseRows = await context.select('license', {
data: {
secretKey: 1,
},
filter: {
id: params.licenseId,
}
}, { dontCollect: true });
const license = licenseRows?.[0];
if (!license?.secretKey) {
throw new OakUserException(
'error::license.secretKeyNotFound',
'project-name',
{ licenseId: params.licenseId }
);
}
return license.secretKey;
},
};
这个 aspect 做的事情非常典型:
- 输入是一个业务参数对象;
- 在后台上下文中执行若干次查询;
- 根据业务条件抛出 Oak 异常;
- 返回最终业务结果。
aspect 如何被执行
在服务端,oak-backend-base/src/AppLoader.ts 中的 execAspect() 会负责:
- 创建并初始化后台
context; - 找到对应名称的 aspect 函数;
- 执行 aspect;
- 调用
context.refineOpRecords(); - 提交事务;
- 返回
{ result, opRecords, message }。
因此,aspect 并不是一个“裸函数调用”,而是 Oak 事务体系中的一个标准入口。
aspect 和 endpoint 的区别
这两个概念经常被新手混在一起。
可以先这样理解:
aspect:Oak 内部命名业务接口,通常由 Oak 的 connector / cache / feature 体系去调用;endpoint:更贴近 HTTP 层,直接暴露为路由入口。
如果一个能力主要服务于 Oak 自己的前端运行时,那么优先写成 aspect;如果一个能力本来就是开放 HTTP 接口,或者需要处理下载流、第三方回调等更底层的请求细节,再考虑 endpoint。
什么时候该写 aspect
适合写成 aspect 的情况通常有:
- 一次业务动作涉及多个实体;
- 逻辑不适合挂在单一实体的
action上; - 需要返回聚合结果,而不是单纯增删改查;
- 需要把一段复杂服务逻辑封装成前端可直接调用的方法。
例如,一个管理后台总览 aspect 可以一次性统计多个实体并返回一个明确的汇总结构,而不必让前端分别发起多次底层查询。
在前端如何使用 aspect
Oak 前端通常通过两种方式间接使用 aspect:
- 直接使用
cache.exec('aspectName', params); - 在
feature中通过createService(...)再包一层,更方便组件调用。
当前 oak-general-business/template/featuresIndex.ts 也使用这一模式:
const aspect = createService<EntityDict, MergeAspectDict>(cache);
这样一来,前端就可以把 aspect 当成一个有类型的服务对象来使用。
使用 aspect 时的一条经验
aspect 很适合表达“一个完整业务动作”,但不适合承载系统底层一致性规则。
换句话说,aspect 可以组织流程,但不要拿它替代 checker 和 trigger。否则一旦这个流程之外还有别的入口触发同样的数据变化,规则就会失效。