文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载可视化搭建框架在完成画布渲染与数据流设计后还需提供一套方便易用的内置状态与方法才能真正让业务侧开箱即用。本篇以本仓库《可视化搭建》系列为背景系统拆解内置 API 的完整清单哪些状态与方法必须有、哪些建议有以及它们各自的类型签名、设计动机与底层实现思路含 O(1) 时间复杂度优化。读完本文你将掌握一套可直接复用的可视化搭建 API 设计范式理解以组件树为核心、以组件 ID 为操作入参、内部转化为树操作的通用原则并能据此评估或设计自己的搭建框架。一、为什么画布设计完成后还需要内置 API在设计好画布与组件数据流体系后参见 画布与组件元信息数据流理论上主体功能已经完成组件树描述结构、组件元信息描述渲染、数据流打通 UI 与元信息。但开发者在搭建业务时仍需频繁执行增删改查组件、获取选中项、撤销重做、获取最终 props等通用操作——如果这些能力全部由业务各自实现不仅重复造轮子还会导致插件、自定义组件无法以统一标准读取框架状态。因此需要内置一些状态与方法。但内置的边界必须克制内置状态与方法必须寻求业务的最大公约数极具抽象性添加需慎重一旦内置了不合理的 key会成为框架的历史包袱。接下来的核心问题是哪些 API 是必须有的哪些是建议有的这构成了本文的主线。在深入清单之前先明确这两类 API 的引用方式因为它是理解所有 API 语义的前提。二、状态与方法的两种引用方式2.1 状态可变数据两种引用途径状态是可变的引用方式有两种方式一任意 React 组件内通过useDesigner访问。当状态变化时会触发所在组件重渲染const { componentTree } useDesigner((state) ({ componentTree: state.componentTree, }));useDesigner还支持第二个参数compare自定义对比函数默认使用shallowEqual详见 画布与组件元信息数据流useDesigner( (state) ({ componentTree: state.componentTree, }), isEqual );方式二任意组件元信息内通过selector访问。当状态变化时会触发不同行为——在runtimeProps中会触发组件重渲染在fetcher中会触发重新查询const tableMeta { /** ... */ runtimeProps: ({ selector }) { const { componentTree } selector(({ state }) ({ componentTree: state.componentTree, })); return { componentTree }; }, };值得注意的是selector内部拿到的state或props其实都是Proxy 代理对象框架借此记录依赖关系实现按需重执行详见 自动批处理与冻结 中的性能优化设计。2.2 方法不可变引用两种引用途径方法引用不可变引用方式同样有两种方式一任意 React 组件内通过useDesigner访问。因为方法引用恒定所以不会导致组件重渲染const { addComponent } useDesigner();方式二任意组件元信息内通过回调直接访问const tableMeta { /** ... */ runtimeProps: ({ addComponent }) {}, };由于方法引用不变元信息回调如runtimeProps可以直接解构使用不会带来额外的重渲染开销。三、内置状态清单3.1 componentTree —— 必须有类型ComponentInstance评价必须有描述完整组件树 JSON 结构。这里有一个关键设计非受控模式下组件树存储在Designer /实例内部受控模式下组件树存储在外部状态。但框架允许两种模式都通过componentTree状态访问这样在开发可视化搭建应用的过程中就不用关心受控或非受控模式了即一套代码同时兼容受控与非受控模式。这得益于数据流层对受控/非受控的统一抽象见 画布与组件元信息数据流受控模式通过Designer actions{{...}} state{{...}} /对接外部 Redux/Dva/Zustand非受控模式通过createMiddleware直接以 Designer 数据流为项目数据流两种模式对外暴露的状态与方法完全一致。3.2 selectedComponentIds —— 建议有类型string[]评价建议有定义当前选中组件实例 id 列表。虽然业务也能自行定义选中状态但选中组件是可视化搭建中的常见行为以后定义插件、自定义组件也许都会读取当前选中的组件如果框架定义了此通用 key那么插件和自定义组件就可无缝结合到任意业务代码里。反之如果在业务层定义该状态插件或者自定义组件也不知道如何标准的读取到当前选中的组件。这是通用 key 即协议的典型体现——框架内置该 key 的价值不在于省一行代码而在于让插件生态拥有可对接的标准。3.3 canUndo、canRedo —— 建议有类型boolean评价建议有描述当前状态是否能撤销或重做。它们需要与内置方法undo()、redo()一起提供属于有了更好的状态。但作者也明确指出其局限比如你的应用分了多个 sheet每个 sheet 内是一个画布实例而你希望撤销重做可以跨 sheet那就不适合用单实例提供的方法了。这提示 API 设计者内置状态与方法应以单画布实例为粒度跨实例的复合能力应交由业务层基于基础 API 自行组装。四、内置方法清单上必须有的核心方法4.1 状态基元getState() 与 setState()getState(): State // 必须有 setState(state: State): void // 必须有getState()获取应用全部状态包括内置与业务自定义。setState()更新应用全部状态包括内置与业务自定义。在 画布与组件元信息数据流 中明确说明Designer 内部采用最朴素的 Redux 管理状态提供最基础的getState与setState获取与修改状态基于它们封装业务函数即可。这也解释了为什么createMiddleware的第二个参数能拿到setState来封装自定义 hooks如setUserName——所有高层 API 最终都收敛到这两个基元。4.2 组件树专属getComponentTree() 与 setComponentTree()getComponentTree(): () ComponentInstance // 必须有 setComponentTree(callback: (now: ComponentInstance) ComponentInstance): boolean // 必须有为什么有了componentTree状态还要getComponentTree()方法因为很多回调函数并不依赖组件树重渲染而仅仅在触发时获取其瞬时值此时必须调用该方法。虽然一定程度上可用getState().componentTree代替但作者点明了两者的微妙差异在受控模式下getState().componentTree不一定等价于getComponentTree()因为前者是从Designer /拿组件树而后者直接请求外部状态最新的组件树当组件树受控模式没有及时触发渲染同步时后者值会比前者更新。setComponentTree()与setState()的区别同样体现在受控模式在非受控模式下两者等价于修改componentTree但在受控模式下setComponentTree()会直接透传到外部状态直接修改一手组件树极端情况下表现更稳定。setComponentTree采用回调接收当前组件树、返回新组件树的函数式签名天然适合基于 Immutable 的数据流详见 组件注册与画布渲染 中组件树的 JSON 序列化约束。4.3 组件增删改查四件套这组方法是业务使用频率最高的 API也是组件树操作能力的最小闭环addComponent(componentInstance, parentIdPath?, index?, position?): void // 必须有 deleteComponent(componentIdPath: string): boolean // 必须有 getComponent(componentIdPath: string): ComponentInstance // 必须有 setComponent(componentIdPath, callback): boolean // 必须有addComponent()参数详解基于setComponentTree()实现但因为太常见且意图复杂抽成独立函数componentInstance必选默认把组件实例添加到根节点的children位置parentIdPath可选描述要添加到的父节点 ID。当父节点没有定义组件 ID 时也可以用例如children.0这种组件树路径代替所以名称不叫parentId而是parentIdPathindex可选描述要添加到父节点子元素下标如children的第几项position可选描述要添加到父节点children还是props.header等位置——因为组件实例并不只有children一个容器位置这一点与 容器组件设计 中任意 props key 都可以是子组件实例的理念一致。deleteComponent()的 O(1) 设计componentIdPath可传组件 ID也可传组件树路径真正删除要从树上删。框架内部为了快速从组件 ID 定位到treePath维护了一张映射表因此无论何时调用都是O(1) 时间复杂度。getComponent()与setComponent()分别基于getComponentTree()与setComponentTree()实现——增删都有了查改自然补全。4.4 父子关系getParentId() 与 setParent()getParentId(componentIdPath: string): string // 必须有 setParent(componentIdPath, parentIdPath, index, position): boolean // 必须有getParentId()获取组件的父组件 ID。因为componentTree是树状结构组件实例上找不到父节点所以提供快速找父节点的函数非常必要。内部实现同样不采用遍历而是在解析组件树时就建立好关联映射表保证所有内置方法 O(1)。setParent()调整某个组件的父节点。参数与addComponent()很像只是第一个参数从组件实例改为组件 ID参数含义相同。当画布涉及组件跨父节点移动时这个方法十分关键。作者特别解释了为什么必须内置它当组件跨节点移动时在组件树上操作还是比较复杂的因为移除 添加无论先做哪个都会导致组件树变化从而导致后一个操作位置可能错误。如果每次都重新寻址性能会较差如果想用聪明的方法绕过逻辑还是比较复杂的因此有必要内置该方法。4.5 组件元信息setComponentMeta() 与 getComponentMeta()setComponentMeta(componentName: string, componentMeta: ComponentMeta): void // 必须有 getComponentMeta(componentName: string): ComponentMeta // 必须有setComponentMeta()更新组件元信息。作者提醒提供这个方法对框架挑战较大在提供很多生命周期如runtimeProps、fetcher、init等的情况下随时可能发生组件实例的更新要保证整体逻辑符合预期需要仔细设计。getComponentMeta()获取组件元信息。注意通过Designer /受控/非受控模式注册或直接调用setComponentMeta注册的组件元信息都应该可以正常获取到——这是受控与非受控统一抽象的另一处体现。五、内置方法清单下建议有的便利方法5.1 组件树遍历类getComponents(): ComponentInstance[] // 建议有获取全量组件实例数组打平形式 getTreePath(componentIdPath: string): string // 建议有根据组件 ID 查找组件树上的路径 getParentBy(componentIdPath: string, finder: (parent: ComponentInstance) boolean): string // 建议有getComponents()因为组件树是树状结构业务除了递归遍历外还可以用这种打平形式的组件数组以备不时之需。getTreePath()业务若想自己操作组件树框架提供组件 ID → 树路径的转换方法很合适其内部实现与deleteComponent的 ID→路径映射表同源。getParentBy()基于getParentId()实现一直向上寻找父节点直到finder返回true方便业务向上寻找符合条件的祖先节点。5.2 props 快捷操作类setProps(componentIdPath, callback): boolean // 建议有修改组件实例的 props getProps(componentIdPath): any // 建议有获取组件实例的 props getMergedProps(componentIdPath): any // 建议有获取组件最终混合后的 propssetProps()基于setComponent()实现——修改组件 props 比修改整个组件实例常见得多。getProps()基于getComponent()实现调用频率同样更高。getMergedProps()与getProps()的关键区别组件 props 可能来自组件树也可能来自runtimeProps处理后的结果。为了防止傻傻分不清规定getProps()仅获取组件树上序列化的 props而getMergedProps()获取了包含runtimeProps处理后的最终 props。这与 组件注册与画布渲染 中runtimeProps注入函数、且优先级高于组件树同名 key 的设计一脉相承。5.3 选中与撤销重做setSelectedComponentIds(ids: string[]): void // 建议有修改内置状态 selectedComponentIds undo(): void // 建议有撤销 redo(): void // 建议有重做既然提供了selectedComponentIds内置状态就强烈建议配套setSelectedComponentIds()修改方法虽然也可通过setState()更新该 key 实现。同理如果提供了canUndo、canRedo内置状态那么一定要提供undo()、redo()内置函数二者成对出现。5.4 DOM 相关getComponentDom(componentIdPath: string): HTMLElement // 建议有 afterDomRender(componentIdPath: string, callback: () void): Promise // 建议有getComponentDom()根据组件 ID 获取 DOM 实例。框架最好通过一些技巧如createPortal、ref 注入等让组件即便不用forwardRef也能拿到 DOM只要组件存在 DOM 即可通过该方法拿到非常方便。afterDomRender()因为组件 DOM 依赖渲染不能保证调用getComponentDom时 DOM 真的完成了渲染因此将时机放在afterDomRender()回调后保证一定可以拿到 DOM。5.5 组件元信息批量操作getComponentMetas(): ComponentMeta[] // 建议有批量获取所有已注册的组件元信息 clearComponentMetas(): void // 建议有清空所有组件元信息作者对两者的评价很朴素说不定业务会有什么特别的用途建议提供。这体现了内置 API 的另一条取舍标准实现成本低、潜在收益不确定但非负的 API 可以纳入建议有区间。六、总结内置 API 的三条设计原则回顾本章设计内置 API 的思路可以归纳为三条原则它们共同保证了框架核心极简、外围便利、整体稳定从组件树核心概念发散围绕componentTree这个核心概念设置必要 API再补充逻辑复杂或使用很方便的推荐 API。整个清单的骨架是状态基元getState/setState→ 组件树基元getComponentTree/setComponentTree→ 增删改查add/delete/get/set props 快捷→ 关系与便利方法。组件 ID 作为统一操作入参内部转化为树操作虽然组件树是树状结构但内置 API 考虑易用性所有操作都以组件 ID或 ID 路径作为参数内部实现时转化为组件树操作并内置 O(1) 时间复杂度的优化ID→treePath 映射表、父节点映射表均在组件树解析时预构建。核心 API 只有寥寥几个其余皆以核心 API 为基础实现addComponent基于setComponentTree、getComponent基于getComponentTree、setProps基于setComponent、getMergedProps基于getComponent……这种便利 API 全部建立在少量核心 API 之上的做法让框架核心更稳定同时框架大部分 API 只定义实现规则业务利用核心 API 拥有更大的实现自由。这三条原则在后续章节中被反复验证在 场景实战 中上卷下钻、Tabs 组件、富文本内嵌组件、任意协议扩展都只用了runtimeProps、valueRelates、setValue、onReadComponentMeta等少量 API 组合在 keepAlive 模式 与 ComponentLoader 与动态组件 中框架仅通过新增渲染层ComponentLoader、keepAlive参数就扩展了能力而未动摇任何基础 API 的语义。可以说本章给出的不仅是一份 API 清单更是一套可视化搭建框架的最小可用协议状态与方法统一抽象、组件树为核心、O(1) 映射优化、便利 API 收敛于核心 API——只要熟悉了这套规范业务侧几乎可以仅根据搭建行为一眼猜出底层是基于哪些 API 封装实现的维护成本与理解成本随之大幅降低。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐Higress 插件生态指南7 个热门社区扩展快速上手Higress 插件生态指南7 个热门社区扩展快速上手 本文基于开源项目 HigressAI Native API Gateway面向新手推荐并讲解 7API网关后端云原生LLM 网关人工智能MCP 服务如何永久保存微信聊天记录WeChatMsg完整使用指南让数据真正属于你如何永久保存微信聊天记录WeChatMsg完整使用指南让数据真正属于你 你是否曾为微信聊天记录的丢失而懊恼那些珍贵的对话、重要的约定、温馨的回忆是否因为手前端精读周刊Designer 可视化搭建的 ComponentLoader 与动态组件全解析前端精读周刊Designer 可视化搭建的 ComponentLoader 与动态组件全解析 本文来自《前端精读周刊》可视化搭建系列围绕 可视化搭建/2文档技术博客教程上一篇PlotJuggler终极指南免费开源的时间序列可视化神器下一篇Scanpy单细胞分析终极指南从入门到精通的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考