当前位置: 首页 > Web前端 > HTML

精读《Headless 组件用法与原理》

时间:2023-03-28 14:55:33 HTML

Headless组件没有UI组件,框架只提供逻辑,UI交给业务实现。这样做的好处是业务有巨大的UI定制空间,而对于框架来说,只考虑逻辑更容易覆盖更多的场景,满足更多开发者的不同需求。下面以headlessui-tabs为例,看看它的用法,阅读源码。无头标签概述最简单的用法如下:import{Tab}from"@headlessui/react";functionMyTabs(){return(Tab1选项卡2选项卡3内容1内容2Content3);}以上代码没有做任何逻辑自定义,只使用了Tab和提供的标签改变tabs的结构如前所述,此时框架可以提供最基本的tabs切换功能,即按顺序,当Tab被点击时,内容切换到对应的Tab.Panel。这个时候没有额外的UI样式,连tab的选中状态都没有。如果需要进一步自定义,则需要使用框架提供的RenderProps能力获取状态,然后自定义业务层。例如选中状态:{({selected})=>(Tab1)}要实现选中状态,需要自定义UI。如果使用RenderProps展开,那么Tab应该不提供任何UI,所以as={Fragment}表示该节点是逻辑节点而不是UI节点(不生成dom节点)。同样,框架将tabs组件拆分为Tab标题区Tab和Tab内容区Tab.Panel,每一部分都可以通过RenderProps进行自定义,框架已经根据业务逻辑规定了每一部分可以做哪些逻辑扩展,比如Tab提供了selected参数,告知当前Tab是否处于选中状态,业务可以根据它来高亮UI,但是框架没有包含怎么做高亮,所以体现了tabs组件的可扩展性,但是相应的业务开发成本也较高。以Headless的可扩展性为例:如果业务方想要自定义Tab标题,我们可以将Tab.List包裹在一个更大的标题容器中,在不破坏原有Tabs逻辑的情况下,在任意位置添加标题jsx。然后把这个组件作为一个普通的业务组件来使用。查看更多配置参数:控制某个Tab是否可以编辑:Tab2Tab切换是否手动按Enter或Space键:默认激活Tab:<标签。GroupdefaultIndex={1}>MonitoractivatedTabchanges:{console.log('Changedselectedtabto:',index)}}>受控模式:用法介绍到这里。细读可知,Headless组件在React场景中更多地使用RenderProps来提供UI扩展能力,因为RenderProps不仅可以自定义UI元素,还可以获取当前上下文的状态,天然适合UI自定义。还有一些headless框架比如TanStacktable也提供了Hooks模式,比如:consttable=useReactTable(options)returnHooks模式的好处是有没有RenderProps回调那么多,代码层面看起来舒服很多,而且Hooks模式也逐渐在其他框架中得到支持,使得组件库的跨框架适配成本相对较低。但是Hooks方式会在React场景中造成不必要的全局ReRender。相比之下,RenderProps只会将重新渲染限制在回调函数中,RenderProps在性能上更胜一筹。分析的差不多了,我们来看headlessui-tabs的源码。首先要封装好组件,解决组件内部通信问题,即Tab.Group封装后为什么Tab和Tab.Panel可以链接起来?他们必须访问公共上下文数据。答案是Context:首先在Tab.Group中使用ContextProvider包裹一层context容器,并封装一个Hook从容器中提取数据://导出的别名叫做Tab.GroupconstTabs=()=>{return({render({ourProps,theirProps,slot,defaultTag:DEFAULT_TABS_TAG,name:"Tabs",})});};//提取数据的方法functionuseData(component:string){letcontext=useContext(TabsDataContext);if(context===null){leterr=newError(`<${component}/>缺少父组件。`);if(Error.captureStackTrace)Error.captureStackTrace(err,useData);抛出错误;}returncontext;}所有的Tab、Tab.Panel、Tab.List等子组件从useData获取数据,这些数据可以从当前最近的Tab.Group上下文获取,这样多个tab之间的数据可以相互隔离.另外一个重点是RenderProps的实现。其实我们早在75就讲到RenderProps的实现了。精读《Epitath 源码 - renderProps 新用法》。今天我们就来看看headlessui的封装。核心代码精简如下:function_render(props:Props&{ref?:unknown},slot:TSlot={}asTSlot,tag:ElementType,name:string){让{as:Component=tag,children,refName='ref',...rest}=omit(props,['unmount','static'])让resolvedChildren=(typeofchildren==='function'?children(slot):children)作为|反应元素|ReactElement[]if(Component===Fragment){returncloneElement(resolvedChildren,Object.assign({},//过滤掉未定义的值,这样它们就不会覆盖现有值mergeProps(resolvedChildren.props,compact(omit(休息,['ref']))),dataAttributes,refRelatedProps,mergeRefs((resolvedChildrenasany).ref,refRelatedProps.ref)))}returncreateElement(Component,Object.assign({},omit(rest,['ref']),组件!==片段&&refRelatedProps,Component!==Fragment&&dataAttributes),resolvedChildren)}首先,为了支持Fragment模式,在制定as={Fragment}时,直接使用resolvedChildren作为子元素,否则会作为dom使用carriercreateElement(Component,...,resolvedChildren)渲染体现RenderProps是resolvedChildren处理的部分:letresolvedChildren=typeofchildren==="function"?儿童(插槽):儿童;如果children是一个函数类型,作为一个函数执行,并传入上下文(这里是槽),返回值是一个JSX元素,这就是RenderProps的本质。看上面Tab.Group的用法:render({ourProps,theirProps,slot,defaultTag:DEFAULT_TABS_TAG,name:"Tabs",});其中slot是当前RenderProps可以获取到的context,比如在Tab.Group中提供selectedIndex,在Tab中提供selected等,在不同的RenderProps位置提供方便的context,更加人性化。例如,如果在Tab中已知Tab的index和selectedIndex,那么为用户提供组合变量selected可能比单独提供这两个变量更方便。总结下面总结一下Headless的设计和使用思路。作为框架作者,首先要分析这个组件的业务功能,抽象出应该拆分成哪些UI模块,并使用RenderProps以UI无关的方式提供这些UI模块,精心设计每个UI提供的状态模块。作为用户,了解这些组件支持哪些模块,每个模块提供了哪些状态,并根据这些状态实现相应的UI组件,以响应这些状态的变化。由于框架内置了最复杂的状态逻辑,对于各种UI状态的业务,甚至可以为每个组件重写UI风格。对于风格稳定的场景,业务也可以按照Headless+UI的方式将UI组件整体打包,提供给各个业务场景调用。讨论地址是:Jingdu《Headless 组件用法与原理》·Issue#444·dt-fe/weekly想参与讨论的请戳这里,每周都有新话题,周末或周一发布。前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)