设计完画布和组件数据流系统后,理论上已经完成了主要功能,但是缺少方便易用的API,所以一些内置状态和方法是需要的。但是内置的state和method必须求业务的最大公约数,非常抽象,需要谨慎添加。下面我们从musthave和suggestion的角度,看看哪些API需要构建成一个可视化的构建。StateState是可变的,可以通过以下两种方式引用。第一个是在任何React组件中通过useDesigner访问的,当状态发生变化时,它会触发组件的重新渲染:const{componentTree}=useDesigner((state)=>({componentTree:state.componentTree,}));第二种通过selector在任何组件meta信息中访问,state改变时会触发不同的行为,比如在runtimeProps中会触发组件重新渲染,在fetcher中会触发重新查询:consttableMeta={/**...*/runtimeProps:({selector})=>{const{componentTree}=selector(({state})=>({componentTree:state.componentTree,}));返回{组件树};},};componentTree评估:必须具有类型:ComponentInstance描述完整的组件树JSON结构。在非托管模式下,组件树存储在实例内部,而在托管模式下,组件树存储在外部状态。但是我们让两种模式都可以访问这个状态,这样在开发可视化和构建应用程序的过程中,你就不用关心受控模式和非受控模式,也就是一套代码同时兼容受控和非受控。不受控制的模式。selectedComponentIdsevaluation:Suggestedtype:string[]定义当前选中组件的实例id列表。虽然这个状态业务也可以定义,但是在可视化构建中选择组件是一种常见的行为。稍后定义插件和自定义组件可能会读取当前选择的组件。如果框架定义了这个通用键,那么插件和自定义组件就可以无缝集成到任何业务代码中。反之,如果状态定义在业务层,插件或自定义组件不知道如何以标准方式读取当前选中的组件。canUndo,canRedo评价:建议类型:boolean描述当前状态是否可以撤销或重做。这个状态需要和内置方法undo()和redo()一起提供,这是一个“更好”的状态。但有时也会遇到麻烦。比如你的应用被分成多个sheet,每个sheet都是一个canvas实例,你想跨sheet进行undo和redo,就不适合使用单实例提供的方法。方法状态引用是不可变的,引用方法有如下两种。第一个是在任何React组件中通过useDesigner访问的,它不会改变,因此不会导致组件重新渲染:const{addComponent}=useDesigner();第二个是通过任何组件元信息中的回调访问的:consttableMeta={/**...*/runtimeProps:({addComponent})=>{},};getState()评估:musthavetype:()=>State获取应用程序的所有状态,包括内置和业务定制。setState()评估:必须具有类型:(state:State)=>void更新应用程序的所有状态,包括内置和业务自定义。getComponentTree()评估:必须具有类型:()=>ComponentInstance返回当前组件树。并不是说componentTree状态就万事大吉了。很多回调函数并不依赖于组件树的重新渲染,而只需要在触发时调用这个方法来获取它的瞬时值。虽然这个方法在一定程度上可以用getState().componentTree代替,但是组件树的概念如此重要,单独定义一个方法并不会增加理解成本。另外,在受控模式下,getState().componentTree不一定等同于getComponentTree(),因为前者从中获取组件树,而后者直接请求外部状态中最新的组件树。当组件树处于受控模式时,没有及时触发渲染同步时,后者的值会比前者更新。setComponentTree()评估:必须具有类型:(回调:(now:ComponentInstance)=>ComponentInstance)=>boolean更新当前组件树。非受控模式下,相当于setState()修改componentTree,但非受控模式下,会透传给外部state,直接修改一手组件树,所以极端情况下性能更稳定。addComponent()评估:必须具有类型(componentInstance,parentIdPath?,index?,position?)=>void添加组件实例。基于setComponentTree()的实现,但由于过于通用,意图比较复杂,还是需要抽离出来一个独立的函数。componentInstance是必需的,组件实例默认添加到根节点的子节点中。parentIdPath是可选的,描述了要添加的父节点的ID。当父节点没有定义组件ID时,也可以用组件树路径代替,比如children.0,所以名字不是parentId,而是parentIdPath。index是可选的,描述了要添加到父节点的子元素的下标,例如要添加到子节点的项目数。position是可选的,描述是否将其添加到父节点children或props.header中。毕竟,组件实例不仅有孩子。deleteComponent()评估:必须具有类型:(componentIdPath:string)=>boolean删除组件实例。基于setComponentTree()的实现,但是同样的原因太常见了,所以单独提供。这里还有一个细节,就是componentIdPath指的是可以传递的组件ID,也可以传递组件树路径,真正的删除必须是从树中删除。为了快速定位组件ID到treePath,框架内部维护了一张映射表,所以使用函数的时间复杂度始终为O(1)。getComponent()评估:必须具有类型:(componentIdPath:string)=>ComponentInstance查询组件实例。基于getComponentTree()实现。“增删”都有,但“查”能少吗?setComponent()评估:必须具有类型:(componentIdPath,callback)=>boolean修改组件实例。基于setComponentTree()的实现,全是“增改查”,只有一个“修改”。setProps()评价:推荐有type:(componentIdPath,callback)=>boolean来修改组件实例的props。基于setComponent()实现,因为修改组件props属性比修改整个组件实例更常见,所以推荐实现。getProps()评价:推荐有一个type:(componentIdPath)=>any来获取组件实例的props。基于getComponent()实现,类似的,调用可能比getComponent()更常见,所以推荐实现。getComponents()评估:推荐有一个type:()=>ComponentInstance[]来获取组件实例的完整数组。由于组件树是树状结构,业务除了递归遍历之外,也可以提供这种组件树以扁平化的形式,以备不时之需。getParentId()评估:必须具有类型:(componentIdPath:string)=>string获取组件的父组件ID。觉得componentTree是一个树型结构,所以不能直接从组件实例中找到父节点,所以提供一个快速找到父节点的功能是非常有必要的。当然,框架内部实现肯定不会使用遍历来寻找父节点。而是在预先解析组件树时建立关联的映射表。所有内置方法的时间复杂度都是O(1)。getParentBy()评价:推荐有一个type:(componentIdPath:string,finder:(parent:ComponentInstance)=>boolean)=>string去寻找父节点,直到找到为止。基于getParentId()的实现,方便业务向上查找符合条件的父节点。setParent()评估:必须具有类型:(componentIdPath,parentIdPath,index,position)=>boolean以调整组件的父节点。参数与addComponent()非常相似,只是第一个由组件实例改为组件ID,参数含义相同。当画布涉及跨父节点移动的组件时,此方法很关键,尽管底层也是基于setComponentTree的。更复杂的场景是当组件跨节点移动时,对组件树的操作还是比较复杂的,因为无论先移除+添加哪个,都会导致组件树发生变化,可能会导致错误后一个操作的位置。如果每次都重新寻址,性能会很差。如果想巧妙的绕过它,逻辑还是比较复杂的,所以构建这个方法是很有必要的。setComponentMeta()评估:必须具有类型:(componentName:string,componentMeta:ComponentMeta)=>void更新组件元信息。提供这个方法其实是对框架的一个比较大的挑战。在提供多个生命周期的情况下,组件实例可能随时更新。为保证整体逻辑符合预期,需要精心设计。getComponentMeta()评估:必须具有类型:(componentName:string)=>ComponentMeta以获取组件元信息。既然可以注册组件元信息,那么就可以获取了。注意通过受控或非受控方式注册的组件元信息,或者直接调用setComponentMeta应该可以正常获取。getComponentMetas()评价:推荐有一个type:()=>ComponentMeta[]批量获取所有注册的组件元信息。可能业务会有一些特殊用途,请提供。clearComponentMetas()评价:推荐有一个type:()=>void清除所有组件元信息。可能业务会有一些特殊用途,请提供。setSelectedComponentIds()评价:推荐有一个type:(ids:string[])=>void修改内置状态selectedComponentIds。如果提供了selectedComponentIds的内置状态,强烈建议提供相应的修改方法。虽然也可以通过setState()更新selectedComponentIds的Key来实现。getTreePath()评价:推荐有一个type:(componentIdPath:string)=>string根据组件ID在组件树上查找路径。可能业务要自己操作组件树,框架提供一种根据组件ID查找组件树路径的方法比较合适。undo(),redo()评估:建议类型:()=>voidundo,redo。如果提供了内置状态canUndo和canRedo,则必须提供内置函数undo()和redo()。getMergedProps()评价:推荐有一个type:(componentIdPath:string)=>any返回组件最终的混合props。由于组件props可能来自组件树或runtimeProps,为了防止混淆,规定getProps()只获取组件树上序列化的props,getMergedProps()获取包含runtimeProps处理后的最终props。getComponentDom()评价:推荐有一个类型:(componentIdPath:string)=>HTMLElement根据组件ID获取DOM实例。框架最好使用一些技巧,让组件即使没有forwardRef也能获取DOM。那么只要组件存在于DOM中,就可以通过这个方法获取到,非常方便。afterDomRender()评估:建议类型:(componentIdPath:string,callback:()=>void)=>Promise当挂载组件ID的DOM实例时,会执行回调。因为组件DOM依赖于渲染,并不能保证在调用getComponentDom时确实渲染了DOM,所以可以将时机放在afterDomRender()之后,以保证能够获取到DOM。总结这一章,我们设计了内置API,设计思路总结如下:从组件树的核心概念展开,设置必要的API,推荐一些逻辑复杂或者容易实现的API使用。组件树虽然是树状结构,但是内置的API需要考虑易用性。所有操作均以组件ID为参数,内部实现时转化为操作组件树,内置O(1)时间复杂度的优化措施。核心API只有几个,其余的API是为了方便而提供的,都是基于核心API实现的,这样框架的核心会更加稳定。框架的大部分API只是一个实现规则,业务使用核心API有更大的实现自由度。讨论地址为:Jingdu《可视化搭建内置 API》·Issue#467·dt-fe/weekly想参与讨论的请戳这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)