forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
2 changed files
with
233 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
接下来用实战来说明该可视化搭建框架是否好用,以下几条原则需要始终贯穿在下面每个实战场景中: | ||
|
||
1. 复杂的业务场景,背后使用的框架 API 是简单的。 | ||
2. 底层 API 并不为业务场景特殊编写,而是具有很强的抽象性,很容易挖掘出其他业务场景的用法。 | ||
3. 所有场景都是基于有限的几条基础规则实现,即背后实现的复杂度不随着业务场景复杂度提升而提升。 | ||
|
||
## 上卷下钻 | ||
|
||
上卷下钻其实是 **组件作用于自身的筛选**。 | ||
|
||
所以上卷下钻背后的实现原理应该与筛选、联动一样。利用 `setValue` 在点击下钻按钮时,修改组件自己的 `value`,然后通过 `valueRelates` 让该组件的联动作用于自身,剩下的逻辑就和普通筛选、联动没有太多区别了,区别仅仅是联动触发源是自己: | ||
|
||
```jsx | ||
import { ComponentMeta } from "designer"; | ||
|
||
const chart: ComponentMeta = { | ||
componentName: "chart", | ||
element: Chart, | ||
// 利用 runtimeProps 将组件 value 映射到 props.value,将 props.onChange 映射为 setValue 修改自身 value | ||
runtimeProps: ({ selector, setValue, componentId }) => ({ | ||
value: selector(({ value }) => value), | ||
onChange: (value: string) => setValue(componentId, value), | ||
}), | ||
// 自己联动自己 | ||
valueRelates: ({ componentId }) => [ | ||
{ | ||
sourceComponentId: componentId, | ||
targetComponentId: componentId, | ||
}, | ||
], | ||
fetcher: ({ selector }) => { | ||
// relates 可能来自自己、其他筛选器组件实例,或者其他图表组件实例 | ||
const relates = selector(({ relates }) => relates); | ||
// 根据 relates 下钻 ... | ||
}, | ||
}; | ||
``` | ||
|
||
上卷下钻就是作用于自身的联动。 | ||
|
||
## Tabs 组件 | ||
|
||
利用组件树解析规则,我们任意找一个 Key 存放每个 TabPanel 的子元素就可以了。 | ||
|
||
我们利用 `props.tabs` 存放 tabs 配置,`props.content` 存放每项 TabPanel 的子组件,因为其顺序永远和 `props.tabs` 保持一致,我们可以简单的使用下标匹配。 | ||
|
||
```jsx | ||
const tabs = { | ||
componentName: "tabs", | ||
element: TabsComponent, | ||
defaultProps: { | ||
// 存放 tabPanel 配置 | ||
tabs: [ | ||
{ | ||
title: "tab1", | ||
key: "1", | ||
}, | ||
], | ||
// 存放每个 tabPanel 内子画布的组件实例 | ||
content: [ | ||
{ | ||
componentName: "gridLayout", | ||
}, | ||
], | ||
}, | ||
}; | ||
``` | ||
|
||
而 TabsComponent 组件实现就完全与平台解耦了,即使用 `props.tabs` 与 `props.content` 渲染即可: | ||
|
||
```jsx | ||
const TabsComponent = ({ content, handleAddTab, handleDeleteTab, tabs }) => ( | ||
<Tabs | ||
editable | ||
defaultActiveTab="1" | ||
onAddTab={handleAddTab} | ||
onDeleteTab={handleDeleteTab} | ||
> | ||
{tabs.map((tab, index) => ( | ||
<TabPane key={tab.key} title={tab.title}> | ||
{content[index]} | ||
</TabPane> | ||
))} | ||
</Tabs> | ||
); | ||
``` | ||
|
||
tabs 使用 treeLike 结构,按照下标存储组件实例。 | ||
|
||
## 富文本内嵌组件实例 | ||
|
||
与 tabs 很像,区别是富文本内嵌入的组件实例数量是不固定的,每一个组件实例都对应富文本某个 block id. 下面是富文本实现代码的一部分: | ||
|
||
```jsx | ||
const SomeRichTextLibrary = (props) => { | ||
// 自定义渲染 block 槽位 | ||
const RenderCustomBlock = useCallback( | ||
(blockId: string) => { | ||
// 渲染组件实例 | ||
return props.blockElements.find( | ||
(componentInstance) => componentInstance.componentId === blockId | ||
); | ||
}, | ||
[props.blockElements] | ||
); | ||
}; | ||
``` | ||
|
||
富文本一般拥有自定义 block 区块的能力,我们只要将 block id 与组件实例 id 绑定,然后将组件实例存储在 `props.blockElements`,就可以轻松匹配到对应组件实例了。 | ||
|
||
其中 `props.blockElements` 的结构如下: | ||
|
||
```json | ||
{ | ||
"blockElements": [ | ||
{ | ||
"componentId": "block1", | ||
"componentName": "chart" | ||
}, | ||
{ | ||
"componentId": "block2", | ||
"componentName": "radar" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
富文本的结构可能如下: | ||
|
||
```json | ||
{ | ||
"type": "rich_text", | ||
"content": [ | ||
{ | ||
"type": "paragraph", | ||
"text": "This is a paragraph of rich text." | ||
}, | ||
{ | ||
"type": "heading", | ||
"level": 2, | ||
"text": "This is a heading" | ||
}, | ||
{ | ||
"type": "block", | ||
"blockId": "block1" | ||
}, | ||
{ | ||
"type": "block", | ||
"blockId": "block2" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
最后两个 block 是自定义区块,通过自定义 `RenderCustomBlock` 来渲染,我们正好可以通过 `blockId` 对应到 `componentId`,在 `props.blockElements` 中找到。 | ||
|
||
富文本的实现思路和 tabs 基本一样,只是查找组件实例的逻辑不同。 | ||
|
||
## 实现任意协议 | ||
|
||
我们也许为了进一步抽象,或对指定业务场景降低配置门槛,在组件树拓展一些额外的 json 结构协议做一些特定功能。 | ||
|
||
以拓展事件配置为例,假如我们需要实现如下协议:每个组件实例信息上拓展了 `events` 属性,通过配置这个属性可以实现一些内置动作,如打开 Modal。这个协议至少要定义触发源是什么 `trigge`r、做什么事情 `type` 以及作用的目标组件 `targetId`: | ||
|
||
```json | ||
{ | ||
"componentName": "button", | ||
"events": [ | ||
{ | ||
"trigger": "onClick", | ||
"type": "openModal", | ||
"targetId": "123" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
如上面的例子,只要定义好触发源、类型和目标组件,就可以在按钮组件 `onClick` 时将目标组件 `visible` 设为 `true`,实现弹出 Modal 的效果。 | ||
|
||
实现思路是,利用 `onReadComponentMeta`,在所有组件的元信息做拓展。比如要拓展这种事件,一般 Trigger 都要绑定在组件 Props 的回调上(如果是全局监听,可以绑定在全局并利用事件机制通信给组件),那就可以通过 `runtimeProps` 进行绑定: | ||
|
||
```jsx | ||
const App = () => ( | ||
<Designer | ||
onReadComponentMeta={(meta) => ({ | ||
...meta, | ||
runtimeProps: (options) => { | ||
const result = meta.runtimeProps?.(options) ?? {}; | ||
const events = options.selector( | ||
({ componentInstance }) => componentInstance.events | ||
); | ||
events?.forEach((event) => { | ||
switch (event.type) { | ||
case "openModal": | ||
// 给组件添加新的 trigger 绑定 | ||
result[event.trigger] = options.setRuntimeProps( | ||
event.targetId, | ||
(props) => ({ | ||
...props, | ||
visible: true, | ||
}) | ||
); | ||
break; | ||
} | ||
}); | ||
return result; | ||
}, | ||
})} | ||
/> | ||
); | ||
``` | ||
除此之外,我们还可以想象有更多的协议可以通过这种方式处理响应,无论何种协议,背后都是基于组件元信息的实现,易懂且单测有保障。 | ||
## 总结 | ||
本文我们总结了三个场景实战: | ||
1. 利用 treeLike 结构在组件内渲染任意数量的子组件实例,如 tabs 或富文本。 | ||
2. 利用组件联动的 API,实现筛选、联动以及上卷下钻。 | ||
3. 利用 onReadComponentMeta 为所有组件元信息统一增加逻辑,用来解读如 `props` 属性中定义的某些规则,进而实现任意协议。 | ||
> 讨论地址是:[精读《可视化搭建 - 场景实战》· Issue #485 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/485) | ||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** | ||
> 关注 **前端精读微信公众号** | ||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg"> | ||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)) |