ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

可视化搭建实战:从上卷下钻到任意协议的场景落地

可视化搭建实战:从上卷下钻到任意协议的场景落地 文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载本文是前端精读周刊「可视化搭建」系列的实战篇。前几篇文章已经完成了理论抽象如何抽象可视化搭建、组件注册与画布渲染组件注册与画布渲染、画布与组件元信息数据流画布与组件元信息数据流、内置 API可视化搭建内置 API与容器组件设计容器组件设计等基础建设。本篇将以「上卷下钻」「Tabs 组件」「富文本内嵌组件实例」「实现任意协议」四个真实业务场景为线索验证这套框架的设计是否足够好用并完整展示每个场景背后的实现原理与代码读者读完可以掌握如何用有限的几条基础规则组合出任意复杂业务功能。贯穿所有场景的三条设计原则在展开实战之前需要先立下三条判断标准它们将贯穿下面每个场景用于衡量框架设计的成色复杂的业务场景背后使用的框架 API 是简单的。业务复杂度不应该由框架 API 的复杂度来承担。底层 API 并不为业务场景特殊编写而是具有很强的抽象性。同一个 API 可以很容易被挖掘出其他业务场景的用法而不是某个场景的专属工具。所有场景都是基于有限的几条基础规则实现。背后实现的复杂度不会随着业务场景复杂度提升而提升即框架核心保持恒定的小体积。带着这三条标准我们逐一审视四个场景看它们分别复用了哪些基础能力。上卷下钻组件作用于自身的筛选上卷下钻是 BI 分析中非常常见的交互点击图表上的某个维度图表就按该维度进一步聚合数据下钻再点击返回图表回到上一层级上卷。在可视化搭建框架中上卷下钻的本质其实是组件作用于自身的筛选。既然是「筛选」那么它的实现原理就应该与普通的筛选、联动完全一致不需要为它发明任何新机制。实现原理复用组件值与值联动回顾框架中两个基础能力详见 组件值与联动组件值每个组件实例都有一个唯一的组件值可以通过setValue(componentId, value)更新通过getValue(componentId)读取。值联动通过组件元信息上的valueRelates声明式定义联动关系目标组件可以通过selector(({ relates }) relates)拿到作用于自己的联动值列表。上卷下钻的做法是点击下钻按钮时利用setValue修改组件自己的value然后通过valueRelates让该组件的联动作用于自身。剩下的逻辑就和普通筛选、联动没有太多区别了——唯一的区别仅仅是联动触发源是自己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 下钻 ... }, };这里的关键点在于valueRelates允许sourceComponentId与targetComponentId指向同一个组件实例。回顾 组件值与联动 中的设计valueRelates是声明式定义的天然满足四个要求支持多对多联动、可随全局状态或自身 props 变化、可以不参与联动链source 与 target 都不指向自己、组件实例销毁时联动关系自动失效。当触发源与目标是同一个组件时就实现了「自己筛选自己」。而fetcher中的relates来源于所有作用于该组件实例的联动源可能来自自己、其他筛选器组件实例或者其他图表组件实例组件只需要统一消费relates就能对「下钻到哪一层」做出响应。因此上卷下钻就是作用于自身的联动。这个场景直接印证了第一条原则——复杂的业务场景下钻、上卷、钻取背后使用的框架 API 只是setValue加valueRelates没有任何为 BI 场景特殊编写的 API。Tabs 组件利用 treeLike 结构渲染任意数量子组件Tabs标签页是搭建平台里最常见的容器组件之一它的特殊性在于每个 TabPanel 都是一个子画布都可以自由摆放组件而且 Tab 数量是动态的。组件树解析规则回顾在 容器组件设计 中已经确立了容器组件的两条约定children约定组件实例的children数组会被解析为组件实例数组传递给组件的props.children。treeLike 约定任何组件 props 上「像组件实例」的属性数组模式下包含componentName都会被解析为 React 实例比如props.header可以是一组组件实例。Tabs 组件正是利用了第二条约定treeLike 结构。我们任意找一个 Key 存放每个 TabPanel 的子元素就可以了不需要任何特殊机制。组件元信息定义利用props.tabs存放 tabs 配置标题、key 等props.content存放每项 TabPanel 的子组件。因为content的顺序永远和tabs保持一致可以简单地使用下标匹配const tabs { componentName: tabs, element: TabsComponent, defaultProps: { // 存放 tabPanel 配置 tabs: [ { title: tab1, key: 1, }, ], // 存放每个 tabPanel 内子画布的组件实例 content: [ { componentName: gridLayout, }, ], }, };组件实现完全与平台解耦TabsComponent 组件实现就完全与平台解耦了它只认识props.tabs与props.content两个普通 props渲染即可const TabsComponent ({ content, handleAddTab, handleDeleteTab, tabs }) ( Tabs editable defaultActiveTab1 onAddTab{handleAddTab} onDeleteTab{handleDeleteTab} {tabs.map((tab, index) ( TabPane key{tab.key} title{tab.title} {content[index]} /TabPane ))} /Tabs );这里handleAddTab、handleDeleteTab这类函数型 props 由runtimeProps注入详见 组件注册与画布渲染 中「给组件注入函数」一节因为组件树是必须可序列化的 JSON函数不能存在于组件树中只能定义在组件元信息上。tabs 使用 treeLike 结构按照下标存储组件实例。增删 Tab 时业务只需要同步操作props.tabs与props.content两个数组保持下标一致即可保证渲染正确。整个过程没有引入任何框架新概念只是把「组件 props 的某个 key 可以是组件实例」这条规则用了起来。富文本内嵌组件实例block id 与组件实例 id 绑定富文本是内容编辑场景的常客。与 tabs 很像富文本内嵌组件实例的能力也依赖 treeLike 结构区别在于富文本内嵌入的组件实例数量是不固定的而且每一个组件实例都对应富文本某个 block id无法按下标一一对应需要按 id 查找。渲染自定义 block 槽位下面是富文本实现代码的一部分通过自定义渲染函数RenderCustomBlock根据blockId从props.blockElements中找到对应的组件实例并渲染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的结构如下它是一个组件实例数组每个实例以componentId作为唯一标识{ blockElements: [ { componentId: block1, componentName: chart }, { componentId: block2, componentName: radar } ] }富文本自身的文档结构可能如下最后两个block类型的节点就是自定义区块{ 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 基本一样只是查找组件实例的逻辑不同tabs 用下标匹配富文本用 id 匹配。需要指出的是这里的componentId正是 组件注册与画布渲染 中提到的组件唯一 ID 概念——当组件在树中的路径不稳定比如富文本内的 block 顺序可以调整时就必须依赖显式的componentId来定位组件实例。实现任意协议用 onReadComponentMeta 统一拓展前面三个场景都是在「消费」框架的固定能力。这个场景更进一步我们也许为了进一步抽象或对指定业务场景降低配置门槛需要在组件树拓展一些额外的 json 结构协议来做一些特定功能。场景与协议定义以拓展事件配置为例假如我们需要实现如下协议每个组件实例信息上拓展了events属性通过配置这个属性可以实现一些内置动作如打开 Modal。这个协议至少要定义三个要素——触发源是什么trigger、做什么事情type、作用的目标组件targetId{ componentName: button, events: [ { trigger: onClick, type: openModal, targetId: 123 } ] }如上例只要定义好触发源、类型和目标组件就可以在按钮组件onClick时将目标组件visible设为true实现弹出 Modal 的效果。对于使用者来说配置一个 JSON 片段就完成了一个事件绑定配置门槛大幅降低。实现思路基于 onReadComponentMeta 的元信息拓展实现思路是利用onReadComponentMeta在所有组件的元信息上做拓展。在 定义联动协议 中已经介绍过可视化搭建框架支持onReadComponentMeta属性用于在读取所有已注册组件元信息时统一注入逻辑联动协议本身就是基于这种拓展方式实现的。对于事件类协议Trigger 一般都要绑定在组件 Props 的回调上如果是全局监听可以绑定在全局并利用事件机制通信给组件那就可以通过runtimeProps进行绑定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; }, })} / );关键点拆解onReadComponentMeta拿到原始的meta返回一个增强后的新meta这保证了原始组件元信息完全不用感知事件协议的存在。增强后的runtimeProps先调用meta.runtimeProps?.(options)拿到组件原有注入的 props再叠加协议注入的部分实现零侵入式叠加。通过options.selector(({ componentInstance }) componentInstance.events)读取当前组件实例上的events配置selector 保证配置变化时实时更新。通过setRuntimeProps(event.targetId, (props) ({ ...props, visible: true }))以函数式更新的方式修改目标组件的 propstargetId指向哪个组件就修改哪个组件的visible。除此之外我们还可以想象有更多的协议可以通过这种方式处理响应无论何种协议背后都是基于组件元信息的实现易懂且单测有保障。回顾 定义联动协议联动协议也是走同一条路径用valueRelates把协议声明的deps/target关系转化为值联动关系用runtimePropsselector.relates响应联动值变化并注入 props。这说明「基于组件元信息的基础回调解读props属性中定义的某些规则」是一条可复用的通用套路——这与第二条原则完全吻合底层 API 不为业务场景特殊编写却很容易挖掘出新场景的用法。总结本文总结了三个场景实战但背后映射到框架的其实是三组基础能力利用 treeLike 结构在组件内渲染任意数量的子组件实例如 tabs 或富文本。它源自 容器组件设计 中的约定任何 props 上的数组 componentName结构都会被解析为组件实例框架不需要为「容器」发明新类型。利用组件联动的 API实现筛选、联动以及上卷下钻。它源自 组件值与联动 中的组件值 valueRelates上卷下钻只是把联动触发源设为自己的特例。利用onReadComponentMeta为所有组件元信息统一增加逻辑用来解读如props属性中定义的某些规则进而实现任意协议。它源自 定义联动协议 的拓展机制事件协议与联动协议共用同一条实现路径。回到开篇的三条原则来收尾上卷下钻、Tabs、富文本、事件协议每个场景在业务上都堪称复杂但使用的 API 都只是setValue、valueRelates、treeLike 约定、onReadComponentMeta这些基础能力——API 是简单的。没有哪个 API 是专门为某个场景定制的同一套机制同时服务了筛选、联动、下钻、容器、协议拓展等多个场景——抽象性很强。所有场景的复杂度都没有回流到框架核心框架始终只需要维护组件树、组件元信息、数据流这几条基础规则——实现复杂度不随业务复杂度线性增长。反过来说这也给出了一条评估可视化搭建框架的实用标准如果实现某个业务场景时你发现必须为框架新增专属 API、专属数据结构那么说明抽象还不够应该回到组件元信息与数据流这两层看看能否用基础能力组合出该场景。正如 如何抽象可视化搭建 所强调的逻辑层的难点就在于元信息定义足够多、足够通用的生命周期回调函数并且这些回调函数还能尽可能的功能正交。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐EpicDesigner从设计理念到实战落地的可视化搭建框架EpicDesigner从设计理念到实战落地的可视化搭建框架 一、核心价值重新定义可视化搭建体验 EpicDesigner作为一款开源可视化设计工具通过前端低代码UI组件为什么选择DbGate3个步骤掌握跨平台数据库管理的完整解决方案为什么选择DbGate3个步骤掌握跨平台数据库管理的完整解决方案 你是否曾为管理多个数据库而烦恼不同数据库需要不同的客户端工具界面各异操作繁琐。DbGa数据库数据库客户端桌面应用后端上一篇DeepCTR 之 PNNProduct-based Neural Network模型源码级原理剖析与完整参数实战指南下一篇彻底解决Jupyter Notebook中IPYWidget事件触发后print输出失效问题创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表