优维低代码技术专栏,是一个全新的、技术为主的专栏,由优维技术委员会成员执笔,基于优维7年低代码技术研发及运维成果,主要介绍低代码相关的技术原理及架构逻辑,目的是给广大运维人提供一个技术交流与学习的平台。
连载第十七期
《高级指引:Transform 数据转换》
▽
在 Brick Next 中, dataSource+fields 是我们的开发者实践出来的不成文的约定,但是由于没有事先强制约定及深入调研,它的实现在不同的构件里虽然看起来相似,实际则各不相同,这给相关构件的下游使用者来说非常困惑,因此急需统一这些数据转换逻辑。
但由于 Storyboard 是声明式的,它并不擅长做逻辑处理,加上还有构件自行渲染构件并转换数据的场景,我们需要兼顾它们。
因此,我们提供了一套统一的数据转换 transform 模式,它同时用在 useResolves 和构件自行渲染构件并传递和转换数据的场景里。对于后者,我们提供统一的在容器构件中渲染子构件的 React Component: <BrickAsComponent>。
# 示例
例如 useResolves 里可以这样使用:
useResolves:
useProvider: "your.provider"
transform:
descriptions:
- label: "Username"
value: "@{name}"
- label: "Email"
value: "@{email}"
现在推荐使用 Evaluate Placeholders 求值占位符。
transform 是定义了 propertyName => propertyValue 的键值对,它定义了数据请求完成后,将以怎样的形式转换成 properties 输出到构件上。其中 @{...} 是和参数注入类似的占位符,关于它的更多信息,请查看本文下一节内容。
例如对于上述配置,假设 provider 提供的数据为:{ name: "Eve", email: "eve@example.com" },那么该构件将收到属性:
properties:
descriptions:
- label: "Username"
value: "Eve"
- label: "Email"
value: "eve@example.com"
而在构件渲染子构件的地方也是类似处理。
brick: "your.table-brick"
properties:
columns:
- title: "User"
dataIndex: "name"
useBrick:
brick: "your.column-brick"
transform:
descriptions:
- label: "Username"
value: "@{cellData}"
- label: "Email"
value: "@{rowData.email}"
注意这里容器构件为单元格构件(columns)起到了类似 useResolves 的作用:为它们传递数据并提供转换数据的接口,在容器构件里可能是这样实现的:
import { BrickAsComponent } from "@next-core/brick-kit";
import { UseBrickConf } from "@next-core/brick-types";
class BrickTable() {
private renderColumn(useBrick: UseBrickConf, rowData, dataIndex) {
return (
<BrickAsComponent
useBrick={useBrick}
// 为子构件传递数据供 transform 使用
data={{
cellData: rowData[dataIndex],
rowData: rowData
}}
/>
)
}
// ...
}
注意:data 是容器给子构件传递的原始数据(例如表格构件可能需要给单元格构件同时灌入整个行的数据及单个字段的数据)。
# 接口定义
interface ResolveConf {
useProvider?: string;
provider?: string;
method?: string;
args?: any[];
field?: string | string[];
name?: string;
// `transformFrom` 定义取原始数据的哪个字段(使用 `_.get(data, transformFrom)`),不填则为取整个原始数据。
transformFrom?: string | string[];
transform?: GeneralTransform;
}
type GeneralTransform = string | TransformMap | TransformItem[];
interface TransformMap {
[propName: string]: any;
}
interface TransformItem {
from?: string | string[];
to: string | TransformMap;
mapArray?: boolean | "auto";
}
// `<BrickAsComponent />` 将统一完成子构件的渲染,包括数据转换、赋值属性、绑定事件。
type UseBrickConf = UseSingleBrickConf | UseSingleBrickConf[];
interface UseSingleBrickConf {
brick: string;
properties?: Record<string, any>;
events?: BrickEventsMap;
transformFrom?: string | string[];
transform?: GeneralTransform;
if?: string | ResolveConf;
slots?: UseBrickSlotsConf;
}
interface UseBrickSlotsConf {
[slotName: string]: UseBrickSlotConf;
}
interface UseBrickSlotConf {
type?: "bricks";
bricks: UseSingleBrickConf[];
}
# 占位符
@{ ... } 是 transform 使用的占位符。例如,对于 @{someField},解析得到的数据为 _.get(data, "someField"),因此,可以使用诸如 @{some[0].field} 来获得嵌套结构内的数据。另外,可以使用 @{} 来获得整个 data。
关于占位符的更多信息请查看 Placeholders 占位符。
注意:data 在 useResolves 中表示 provider 构件提供的数据,在 则为容器构件自行渲染子构件时传递的数据。
# 语法糖
为了简化一些常见场景的配置,我们提供了一些语法糖(也可以称为潜规则):
# 整个数据赋给单个属性
可以使用 transform: "plainString" 来实现直接将整个数据赋值给单个属性 plainString,此时 transform 等同于 useResolves[].name。
# 数组数据源自动 Map
对于数据源是数组的,transform 会自动使用 Array.prototype.map() 来实现数组映射。例如对于以下配置:
useResolves:
- useProvider: "your.provider"
transform:
tagList:
label: "@{name}"
value: "@{email}"
如果数据源返回的是:
# Provider returns:
- name: "Eve"
email: "eve@example.com"
- name: "Wall E"
email: "wall-e@example.com"
那么构件得到的属性将是:
properties:
tagList:
- label: "Eve"
value: "eve@example.com"
- label: "Wall E"
value: "wall-e@example.com"
# 高级
有时我们会需要转换混合了数组与普通类型的数据。例如对于提供分页能力的查询列表接口,除了处理数组类型的列表 list 外,还可能处理 page pageSize 等普通类型的数据,需要分别按需进行数组自动 map。这时我们可以使用数组形式的 transform 配置。
数组形式的 transform 配置可以按次序执行多次转换,例如对于以下配置:
useResolves:
- useProvider: "your.provider"
transform:
- from: "list"
to:
tagList:
label: "@{name}"
value: "@{email}"
- from: "pageSize"
to: "pageSize"
以上配置对于 list 字段将进行数组自动 map,而 pageSize 则不会。另外可以额外指定 mapArray: false 以不进行数组自动映射。
这里的 from + to 的配置与 transformFrom + transform 的配置是类似的。
如果数据源返回的是:
# Provider returns:
list:
- name: "Eve"
email: "eve@example.com"
- name: "Wall E"
email: "wall-e@example.com"
pageSize: 10
那么构件得到的属性将是:
properties:
tagList:
- label: "Eve"
value: "eve@example.com"
- label: "Wall E"
value: "wall-e@example.com"
pageSize: 10
# 在 useBrick 中使用 slots
现在可以在 useBrick 中使用插槽 slots 了:
brick: "basic-bricks.list-container"
properties:
useBrick:
brick: "your.awesome-brick"
slots:
content:
bricks:
- brick: "your.another-brick"
transform:
textContent: "<% DATA.label %>"
data:
- label: "one"
- label: "two"
插槽配置和普通构件的插槽配置类似,区别是不能设置 type: "routes" 类型的插槽,并且 bricks 中每个子构件配置格式和普通 useBrick 一致,也可以消费和父级构件一样的数据源。
有疑问加站长微信联系(非本文作者)