Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

定义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-aspectexportEntity() 在运行时要求它是非空数组,否则会抛出前置条件异常。因此,现有项目定义导出 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

  • importEntity
  • exportEntity
  • getImportationTemplate

也就是说,port 最终是以 Oak 内置 aspect 的形式暴露出来的。

导入是怎么执行的

oak-common-aspect/src/port.ts 中的 importEntity() 大致流程是:

  1. 根据传入的 id 找到对应 Importation
  2. xlsx 读取上传的 Excel;
  3. 把每个 sheet 转成 JSON 行数据;
  4. 调用你定义的 importation.fn(...) 把这些行转成 Oak create 数据;
  5. chunkSize 分块;
  6. context.operate(entity, { action: 'create', data: chunk }) 批量写入。

因此,真正需要你关心的核心只有一件事:

如何把导入文件中的一行,准确翻译成 Oak 实体上的创建数据。

如果解析过程中出现具体某一行的错误,按框架约定,应抛出 OakImportDataParseException,这样前端才能得到明确的行号和表头信息。

导出是怎么执行的

exportEntity() 的流程则是:

  1. 根据 id 找到对应 Exportation
  2. 用你定义的 projection 分页查询实体数据;
  3. 调用 exportation.fn(...) 把查询结果转换成导出行;
  4. xlsx 生成 workbook 并返回。

导出时,Oak 还支持:

  • maxCount:最大导出条数,当前默认 10000
  • count:每次分页查询条数,当前默认 1000
  • checked:是否在导出前查询总量并在超限时抛错,当前默认 false

checkedfalse 时,maxCount 仍然生效,但超出的结果会被截断,而不是报错。countmaxCount 都必须大于 0

这里还有一个非常值得直接说清楚的点:Exportation.projection 和前端调用 exportEntity(entity, id, filter, ...) 时传入的 filter,都直接复用 Oak 查询语法本身。

也就是说,平时你在列表页、aspect、watcher 里能写的那套:

  • 级联 projection;
  • 父对象 / 子对象 filter;
  • #sqp
  • $expr
  • JSON filter;

放到导出链路里理解方式也是一样的。导出并不是另一套 DSL,它只是把“查询结果 -> 表格行”的这一步标准化了。

获取导入模板

Oak 还内置了 getImportationTemplate。它会根据 Importation.headers 直接生成一份只有表头的 Excel 模板。

这意味着,一个导入功能的最小闭环通常是:

  1. 定义 headers
  2. 提供模板下载;
  3. 提供导入解析;
  4. 提供错误定位。

而这些能力都可以通过同一个 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 更自然。