定义port
port 是 Oak 对批量导入导出能力的抽象。它的目标非常明确:把 Excel 之类的结构化数据交换,也纳入 Oak 的实体和上下文体系,而不是在项目里到处散落手写上传脚本和导出脚本。
如果你的系统里存在下面这些需求,通常就适合用 port:
- 下载一个标准导入模板;
- 上传 Excel 后解析成某个实体的批量创建数据;
- 按过滤条件导出某类实体的数据;
- 让导入导出过程也走 Oak 的权限、上下文和事务体系。
port 的两种定义
oak-domain/src/types/Port.ts 定义了两种 port:
Importation
type Importation = {
name: string;
id: string;
entity: T;
headers: K[];
fn: (data, context, options?) => Promise<CreateMulti['data']>;
};
它负责把上传表格中的每一行,解析成 Oak create 所需的数据。
Exportation
type Exportation = {
name: string;
id: string;
entity: T;
projection: Projection;
headers?: K[];
fn: (data, context?, properties?) => Promise<FormatDataResult[]>;
};
它负责把 Oak 查询出来的实体数据,格式化成导出表格的一行行对象。
需要注意:虽然当前 Exportation 类型仍把 headers 标成可选,oak-common-aspect 的 exportEntity() 在运行时要求它是非空数组,否则会抛出前置条件异常。因此,现有项目定义导出 port 时应始终提供 headers,不要把类型层面的可选理解成运行时也可省略。
编写位置
一般在 src/ports/index.ts 中统一导出:
export const importations: Importation<...>[] = [
];
export const exportations: Exportation<...>[] = [
];
没有导入导出需求时,这两个数组可以为空;有需求时应让每个 id 在全部应用及依赖 port 中保持唯一。挂载阶段发现重复 importation/exportation id 会直接抛错。
port 在运行时如何生效
在服务端,oak-backend-base/src/AppLoader.ts 在挂载应用时会收集应用及依赖包的 port 配置,然后调用 registerPorts(importations, exportations)。正常安装并完成 Oak 依赖初始化后,不需要把业务依赖包的 port 再手工复制到应用数组中。
真正的导入导出实现位于 oak-common-aspect/src/port.ts:
importEntityexportEntitygetImportationTemplate
也就是说,port 最终是以 Oak 内置 aspect 的形式暴露出来的。
导入是怎么执行的
oak-common-aspect/src/port.ts 中的 importEntity() 大致流程是:
- 根据传入的
id找到对应Importation; - 用
xlsx读取上传的 Excel; - 把每个 sheet 转成 JSON 行数据;
- 调用你定义的
importation.fn(...)把这些行转成 Oakcreate数据; - 按
chunkSize分块; - 用
context.operate(entity, { action: 'create', data: chunk })批量写入。
因此,真正需要你关心的核心只有一件事:
如何把导入文件中的一行,准确翻译成 Oak 实体上的创建数据。
如果解析过程中出现具体某一行的错误,按框架约定,应抛出 OakImportDataParseException,这样前端才能得到明确的行号和表头信息。
导出是怎么执行的
exportEntity() 的流程则是:
- 根据
id找到对应Exportation; - 用你定义的
projection分页查询实体数据; - 调用
exportation.fn(...)把查询结果转换成导出行; - 用
xlsx生成 workbook 并返回。
导出时,Oak 还支持:
maxCount:最大导出条数,当前默认10000;count:每次分页查询条数,当前默认1000;checked:是否在导出前查询总量并在超限时抛错,当前默认false。
当 checked 为 false 时,maxCount 仍然生效,但超出的结果会被截断,而不是报错。count 和 maxCount 都必须大于 0。
这里还有一个非常值得直接说清楚的点:Exportation.projection 和前端调用 exportEntity(entity, id, filter, ...) 时传入的 filter,都直接复用 Oak 查询语法本身。
也就是说,平时你在列表页、aspect、watcher 里能写的那套:
- 级联 projection;
- 父对象 / 子对象 filter;
#sqp;$expr;- JSON filter;
放到导出链路里理解方式也是一样的。导出并不是另一套 DSL,它只是把“查询结果 -> 表格行”的这一步标准化了。
获取导入模板
Oak 还内置了 getImportationTemplate。它会根据 Importation.headers 直接生成一份只有表头的 Excel 模板。
这意味着,一个导入功能的最小闭环通常是:
- 定义
headers; - 提供模板下载;
- 提供导入解析;
- 提供错误定位。
而这些能力都可以通过同一个 port 定义串起来。
前端如何调用
Oak 前端已经内置了 Port feature,定义在 oak-frontend-base/src/features/port.ts 中。它暴露了三个方法:
importEntity(entity, id, file, options?, s2jOpts?)exportEntity(entity, id, filter?, properties?, options?)getImportationTemplate(id)
因此,port 的完整链路其实是:
- 后端定义 import/export 规则;
- 前端通过
features.port直接调用; - 中间过程由 Oak 公共 aspect 接管。
什么时候该用 port,而不是 aspect
如果只是一次普通上传,或者你根本不需要“模板 + 批量解析 + 批量创建 + 标准导出”,那直接写 aspect 往往更简单。
但只要你的需求是“一个规范化、可复用、可下载模板的批量导入导出功能”,port 就会比手写 aspect 更自然。