前言最近在一个业务需求中,通过在AntdTable提供的回调函数中写代码实现了这几个功能:每一级缩进指示线子节点远程懒加载每一级都支持分页最终效果大概是这样的:最终效果这篇文章我想说说我在这篇文章中对代码的解耦和写组件的插件机制要求。一些想法。重构思路随着写函数的增加,逻辑耦合在AntdTable的各个回调函数中,引导线的逻辑分散在rewriteColumns和components中。分页的逻辑分散在rewriteColumns和rewriteTree中。加载更多的逻辑分散在rewriteTree和onExpand中,到目前为止,组件中的代码行数达到了300行。看代码的结构,已经相当混乱了:128278;分页逻辑//🏁缩进行逻辑}constcomponents={//🏁缩进行逻辑};constonExpand=async(expanded,record)=>{//🎈加载更多逻辑};return
;};这个时候缺点就暴露出来了,当我想改或者删掉其中一个功能的时候就变得异常痛苦,经常在功能之间跳来跳去寻找。有没有一种机制可以让代码按照功能点聚合起来,而不是分散在各个功能中?//🔖分页逻辑constusePaginationPlugin=()=>{};//🎈加载更多逻辑constuseLazyloadPlugin=()=>{};//🏁缩进线逻辑constuseIndentLinePlugin=()=>{};exportconstTreeTable=rawProps=>{usePaginationPlugin();useLazyloadPlugin();useIndentLinePlugin();return
;};是的,很像VueCompositionAPI做的改进和ReactHook在逻辑上解耦,但是以写这个回调函数的形式,好像不太好做?这时候想起社区里一些开源框架提供的插件机制,貌似可以在每个回调机会注入用户逻辑,而不用深入源码。比如Vite的插件[1]、Webpack的插件[2]甚至是大家熟悉的Vue.use()[3],它们本质上都是对外暴露了一些内部的时序和属性,让用户可以写一些代码介入框架运行的各种机会之中。那么,我们是否可以考虑将“处理每个节点、每个列、每个onExpand”的时机暴露出来,让用户也可以介入这些过程,重写一些属性,调用一些内部方法来实现以上几个功能呢?我们设计插件机制是为了实现这两个目标:逻辑解耦,将各个小功能的代码集成到插件文件中,不与组件耦合,增加可维护性。用户共建,内部使用方便同事共建,开源后方便社区共建。当然,这需要你写的插件机制足够完善,文档足够友好。但是,插件也会带来一些弊端。设计一个完整的插件机制也是非常复杂的。Webpack、Rollup、Redux等插件机制都有很好设计的地方可以借鉴。接下来,我将尝试实现最简单版本的插件系统。源码首先设计插件接口:exportinterfaceTreeTablePlugin
{(props:ResolvedProps,context:TreeTablePluginContext):{/***可以访问每一列并修改*/onColumn?(column:ColumnProps):void;/***可以访问每个节点数据*初始化或添加子节点后执行*/onRecord?(record):void;/***节点扩展回调函数*/onExpand?(expanded,record):void;/***CustomTablecomponent*/components?:TableProps['components'];};}exportinterfaceTreeTablePluginContext{forceUpdate:React.DispatchWithoutAction;replaceChildList(record,childList):void;expandedRowKeys:TableProps['expandedRowKeys'];setExpandedRowKeys:(v:string[]|number[]|undefined)=>void;}我把插件设计成一个函数,这样可以得到最新的每次我执行它的道具和上下文。Context其实就是组件中一些依赖上下文的工具函数,比如forceUpdate,replaceChildList等函数都可以挂在上面。接下来,由于可能会有多个插件,内部可能会有一些解析过程,所以我设计了一个钩子函数usePluginContainer来运行插件:props;constplugins=rawPlugins.map(usePlugin=>usePlugin?.(props,context));constcontainer={onColumn(column:ColumnProps){for(constpluginofplugins){plugin?.onColumn?.(column);}},onRecord(record,parentRecord,level){for(constpluginofplugins){plugin?.onRecord?.(record,parentRecord,level);}},onExpand(expanded,record){for(constpluginofplugins){plugin?.onExpand?.(expanded,record);}},/***暂时只做组件的deepmerge*不处理自定义组件的冲突定义的Cell会覆盖之前的*/mergeComponents(){letcomponents:TableProps['components']={};for(constpluginofplugins){components=deepmerge.all([components,plugin.components||{},props.components||{},]);}returncomponents;},};returncontainer;};目前的流程很简单,调用各个插件函数,然后对外提供封装接口即可。mergeComponent使用deepmerge[4]库合并用户传入的组件和插件中的组件,暂时不处理冲突。然后就可以在组件中调用这个函数来生成pluginContainer:exportconstTreeTable=React.forwardRef((props,ref)=>{const[_,forceUpdate]=useReducer((x)=>x+1,0)const[expandedRowKeys,setExpandedRowKeys]=useState([])constpluginContext={forceUpdate,replaceChildList,expandedRowKeys,setExpandedRowKeys}//使用useImperativeHandle(ref,()=>({replaceChildList,setNodeLoading,})向用户公开工具和方法);//得到pluginContainer后constpluginContainer=usePluginContainer({...props,plugins:[usePaginationPlugin,useLazyloadPlugin,useIndentLinePlugin],},pluginContext);}),在每个进程对应的位置,通过pluginContainer执行对应的hook即可function:exportconstTreeTable=React.forwardRef((props,ref)=>{//省略前面部分代码...//这里我得到了pluginContainerconstpluginContainer=usePluginContainer({...props,plugins:[usePaginationPlugin,useLazyloadPlugin,useIndentLinePlugin],},pluginContext);//递归遍历整个数据调用hookconstrewriteTree=({dataSource,//动态添加子树节点时,需要手动传入父引用parentNode=null,})=>{pluginContainer.onRecord(parentNode);traverseTree(dataSource,childrenColumnName,(node,parent,level)=>{//这里执行插件的onRecordhookpluginContainer.onRecord(node,parent,level);});}constrewrittenColumns=columns.map(rawColumn=>{//这里暴露浅拷贝后的列//防止污染原值constcolumn=Object.assign({},rawColumn);pluginContainer.onColumn(column);returncolumn;});constonExpand=async(expanded,record)=>{//这里执行插件的onExpandhookpluginContainer.onExpand(expanded,record);};//这里获取合并的组件,传递给Tableconstcomponents=pluginContainer.mergeComponents()});之后,我们就可以把之前的分页相关逻辑直接抽象到usePaginationPlugin中:,indentLineDataIndex,}=props;constandlePagination=node=>{//首先添加渲染分页节点};constrewritePaginationRender=column=>{//重写列的渲染//渲染分页};return{onRecord:handlePagination,onColumn:rewritePaginationRender,};};也许你聪明发现这里的插件都是从use开始的是的,这是自定义挂钩的标志。是的,它不仅是一个插件,还是一个自定义的Hook。所以你可以使用ReactHook的所有能力,同时你可以在插件中引入来自各个社区的第三方Hooks来增强能力。这是因为我们通过usePluginContainer中的函数调用来执行每一个usePlugin,完全符合ReactHook的调用规则。懒加载节点相关的逻辑也可以抽象为useLazyloadPlugin:}=context;//处理懒加载节点逻辑constandleNextLevelLoader=node=>{};constonExpand=async(expanded,record)=>{if(expanded&&record[hasNextKey]&&onLoadMore){//处理懒加载逻辑}};返回{onRecord:handleNextLevelLoader,onExpand:onExpand,};};并将缩进线相关的逻辑提取到useIndentLinePlugin中:return{record,...column,};};};constcomponents={body:{cell:cellProps=>(),},};return{components,onColumn,};};至此,主要功能已经精简到150行左右,所有与新功能相关的功能都移到了插件目录下无论是增删改换功能此时的目录结构很容易:目录结构总结本系列介绍扩展Table组件的功能如下:每级缩进指示线远程懒加载子节点每级在开发过程中都支持分页和代码耦合,难度较大维护问题,进而延伸探索组件中插件机制的设计和使用。虽然本文设计的插件仍然是最简单的版本,但原理大致相同。我希望它能启发你。本文转载自微信公众号《前端从进阶到入学》,可关注下方二维码。转载本文请联系前端从进阶到录取公众号。