Skip to content

Commit

Permalink
280
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed May 29, 2023
1 parent 79a3129 commit 906a2ef
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./可视化搭建/279.%E8%87%AA%E5%8A%A8%E6%89%B9%E5%A4%84%E7%90%86%E4%B8%8E%E5%86%BB%E7%BB%93.md">279.自动批处理与冻结</a>
最新精读:<a href="./可视化搭建/280.%E5%9C%BA%E6%99%AF%E5%AE%9E%E6%88%98.md">280.可视化搭建 - 场景实战</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -313,6 +313,7 @@
- <a href="./可视化搭建/276.keepAlive%20%E6%A8%A1%E5%BC%8F.md">276.keepAlive 模式</a>
- <a href="./可视化搭建/278.ComponentLoader%20%E4%B8%8E%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6.md">278.ComponentLoader 与动态组件</a>
- <a href="./可视化搭建/279.%E8%87%AA%E5%8A%A8%E6%89%B9%E5%A4%84%E7%90%86%E4%B8%8E%E5%86%BB%E7%BB%93.md">279.自动批处理与冻结</a>
- <a href="./可视化搭建/280.%E5%9C%BA%E6%99%AF%E5%AE%9E%E6%88%98.md">280.场景实战</a>

### SQL

Expand Down
231 changes: 231 additions & 0 deletions 可视化搭建/280.场景实战.md
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))

0 comments on commit 906a2ef

Please sign in to comment.