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

全新React组件设计理念HeadlessUI

时间:2023-03-28 17:22:28 HTML

作者:Peel其实我第一次接触HeadlessUI是在去年,偶然看到一个非常前沿优秀的组件库----ChakraUI,这个组件库本身就是HeadlessUI的实践者,也是CSS-IN-JS的高手。当时看了之后,对这个概念产生了浓厚的兴趣,同时也恰好有机会在工作中实践(开始重构公司开源组件库的大版本),所以我对这个概念也有一些实践经验。所以今天,我也想和大家分享一下这个前沿技术。另一方面也是个人的技术总结。也希望有兴趣的朋友可以在评论区讨论。契机:ReactHooks的诞生ReactHooks可以说是HeadlessUI实现的基石。为什么这么说呢,这里先说说ReactHooks。我们都知道ReactHooks是什么。ReactHooks诞生于V16.8版本,它让我们的函数组件真正拥有了状态。如下图所示,我们以数字累加功能为例。我们可以看到,对于同样的功能,ReactHooks的写法相比以往类组件的写法,会减少一些代码量。但是仅仅因为这个才支持吗?我们要知道,在Reactv16.8之前,一般情况下,普通的UI渲染都是直接使用函数组件,类组件只有在需要状态或者其他副作用的时候才会使用。两者的分工也是合理的,那么hooks是怎么诞生的呢?难道只是给功能组件赋能?从用户的角度来看,这显然是不合理的,更何况增加了学习成本,还多了一个纠结的选择(功能组件vs类组件)。ReactHooks的意义那么,事情并没有那么简单。我们可以推断,对于hooks来说,它一定是解决了一些“类组件的不足或者痛点”,这里就不卖了,列举2点:1.组件之间的状态逻辑很难复用。过去,状态逻辑的重用往往使用高阶组件来实现。但是缺点也很明显。它需要在原始组件外包裹一层父容器。导致层级冗余甚至嵌套地狱,引来不少吐槽:增强调试难度降低运行效率。相信用过Redux的同学都知道,为了快速管理状态管理和注入组件,都会使用connect来包装组件,但是随着项目的迭代,当你打开DevTools查看的时候,发现DOM经常会比较臃肿。2.复杂组件变得难以理解和维护复杂组件本身很复杂,但是类组件使它们更加难以理解和维护。例如:在一个生命周期函数中,经常会把不相关的逻辑混在一起,或者一组相关的逻辑分散在不同的生命周期函数中,举个例子:在componentWillReceiveProps中,经常会写不相关的props来更新渲染判断逻辑上,对于一个update,往往会有一些无效的执行,会降低执行效率。在componentDidMount中注册事件,在componentWillUnmount中卸载事件,往往很容易忘记甚至写出bug。从长远来看,我们的代码只会变得更糟,更难理解。ReactHooks对组件开发的影响通过ReactHooks,我们可以将组件的状态逻辑分离到自定义的hooks中,将相关的逻辑放在一个Hook中,将不相关的逻辑拆分到不同的hooks中,最后在组件需要的时候再引入,这样实现状态逻辑在不同组件间的复用。正是ReactHooks的诞生使得无头UI组件在技术上成为可能,这也是它最近才流行起来的原因。那么,接下来我们介绍什么是HeadlessUI。什么是无头用户界面?HeadlessUI的定义HeadlessUI在社区中还处于探索和实践阶段。这里我给它做一个简单的定义:HeadlessUI是一套基于ReactHooks的组件开发设计理念,强调它只负责组件的状态和交互逻辑。无论标签和样式如何。它的本质思想其实就是关注点分离:将组件的“状态和交互逻辑”与“UI表现层”解耦。HeadlessUI组件从物理角度来看,HeadlessUI组件是一个ReactHook。从外观上看,HeadlessUI组件实际上是一个不渲染任何东西的组件。为什么会有HeadlessUI?那么为什么我们需要一个不渲染任何东西的组件呢?这里我们还是以数字加减法的功能为例,首先思考一个数字加减法Counter组件的设计与实现。传统版本组件的设计痛点按照传统的模式,我们可能会直接写出一个名为Counter的组件,然后直接用它来渲染。组件的功能是通过props来设置的,比如一个不可控的初始数字值。那么无法满足的痛点是什么?我们这里随机取一个场景,然后从组件用户、维护者、服务产品的角度来分析。1、用户——如何满足高度定制化的业务场景?现在我们的业务有这样一个需求:要求左右加减按钮支持长按悬浮显示Tooltip提示。其实从产品的角度来说,这个需求很简单,就是提升交互体验。但是如果按照以前传统的组件化设计,就会很头疼。都暴露在组件库中了(假设哈),如何侵入它,给加减按钮添加Tooltip呢?实际上,对于组件等定制化业务场景的诉求,我们一般的解决方案可能是这样的:但是随着方案选择的后期,我们的成本越来越高,脸上的痛面具也越来越多明显的。2.Maintainer-ComponentAPI越来越复杂,功能扩展&向后兼容苦恼?对于维护者来说,如果他想满足这样的需求,那么他可能会这样做。一开始,要求比较简单。我们可以通过添加API来动态注入要实现的功能。针对以上需求,我们可以添加xxxButtonTooltipText等API来实现Tooltip文案的配置;一周后,我们需要加减按钮来支持Icon自定义,我们可能会添加xxxButtonText等API来满足需求;又过了2周,我们要支持Tooltip显示方向配置,避免挡住核心内容的显示,我们可能会添加xxxButtonTooltipPlacement。..日复一日,组件API的数量迅速增加。最后,maintainer发现实在忍不住了,决定尝试一劳永逸的使用RenderProps设计,于是添加了xxxButtonRender来支持加号和减号按钮的自定义功能渲染。我们发现,通过这样做,一个简单的组件会变得越来越复杂。不仅存在功能冗余,后面还要考虑功能扩展和向后兼容,脸上痛苦的面具逐渐显现。另外,对于用户来说,当他们想要使用一个组件时,发现有好几页的API,他们会稍稍感叹一下。函数检索难度大,大部分可能用不到,性能优化难以下手。3.产品:如何快速打造好用的定制化品牌UI?对于一个产品来说,最重要的一点就是塑造产品本身的品牌形象和产品特性。这对于用户最直接接触的UI交互尤为重要。那么如何快速打造好用的定制化品牌UI呢?还是以数字加减法器为例,那么它的易用性可能体现在它比较完备和易用的能力上。点击加减按钮:数字加减步AccessibilityAccessibility数字值最大值和最小值控制……...对于它的定制化,可能体现在它的UI视图层的差异化上。如下图,光是Counter这样的小组件就有多种UI形式,更不用说其他更复杂的组件了。HeadlessUI的解决方案从上面的分析可以看出,UI是一个自由度非常高的东西,构建UI是一个非常品牌化和定制化的体验。那么,是否可以只复用组件的交互逻辑,完全自定义布局和样式呢?显然,这就是HeadlessUI的用途。对于HeadlessUI组件,我们首先要做的是分析提取组件的状态和交互逻辑。对于Counter组件,它的状态逻辑大致如下:我们将这些状态逻辑汇聚到一个名为useCounter的ReactHook中。它接受用户传入的功能性API设置,并返回一组新的已处理API。对于用户来说,我们只需要将返回的API赋值给我们想要赋值的标签,就可以得到一个只有交互能力的headless组件。最后结合设计稿还原UI,编写自定义样式,最终实现了一个全新的数字加减法组件;此外,我们还可以重新排列标签,然后改变样式,绝对定位按钮。一段时间后,我们终于可以实现一个数字输入框组件;此外,我们还可以基于它进行封装。比如原来的最大值表示总页数,插入到标签中间,再改一下样式,就实现了。寻呼机组件的迷你版。可以看到,通过HeadlessUI的设计思路,我们最终产生了一个名为useCounter的ReactHook。通过它,我们不需要关心组件中最复杂、最常用的部分——交互逻辑,而是交给组件维护者来管理;而对于频繁变更需要定制的UI部分则完全由我们自行决定,从而最大程度满足业务高度定制化扩展的诉求,同时尽可能充分复用代码。HeadlessUI的优缺点这里简单回顾一下HeadlessUI的优缺点,以及目前推荐的适用场景,以方便技术选型和学习。优点有非常强大的UI定制空间,支持高度定制化扩展。可以看到headless的优势也很明显,因为比较抽象,所以有非常强大的自定义扩展能力:支持标签布局,元素组合,内容插入,样式定义等都可以满足。最大化代码复用,减小包体积从上面可以看出,组件的状态逻辑可以尽可能的最大化复用,帮助我们减小包体积,增强整体的可维护性。它对编写单元测试很友好,因为它基本上是逻辑性的。可以快速模拟事件回调、React运行管理等,实现单元测试编写和回归;而UI部分一般比较容易改,不容易出bug,可以避免测试。缺点是对开发者能力要求高,需要很强的组件抽象设计能力。抽象层次越高,越难写。对于这种headless组件,我们重点关注组件API设计和交互逻辑的分离,这很考验开发者的组件设计能力。使用成本较高,不建议在简单的业务场景下完全自定义UI层。有一定的开发成本,需要对投入产出进行评估。对于没有特别高要求的2b业务,推荐使用AntDesign。用于开发的UI规范组件库。HeadlessUI社区生态的生态及展望关于组件,国外已经有一些探索和实践的案例,如React-Popper、React-Hook-Form、TanStack-Table。三者是元件库的“三难”。他们的明星(数万)和活跃度都非常高。未来基于headlessUI设计实践的组件只会越来越多。关于组件库,目前我看到比较好的实践是Chakra-UI组件库。整个组件库采用分层架构(这里以数字输入框组件为例):底层使用了HeadlessUI的套路模式,并暴露了相关的ReactHook,保证了整个组件高度定制化和扩展性的需求组件可以最大化。上层提供了类似AntDesign的组件,有一个默认的UI,但不同的是,每个组件都是由更小的、必要的原子组件组成,可以直接导入使用,从而保证大型一些简单或者普通的场景可以实现很快就满意了。注:其实就是将一个组件拆分成多个必要的原子组件,这其实是HeadlessUI的一种实用形式。将交互逻辑生效的API直接绑定到必要的元素标签上,然后暴露为原子组件。标签的排版和样式修改也可以完全由用户自定义。此外,在ReactNext2022大会上,嘉宾们也分享了与HeadlessUI相关的想法,目前整个社区都处于持续发酵的阶段。未来展望我个人认为HeadlessUI是未来React组件库底层的最佳实践。对于组件库,也许大家不需要“看书参考”,而是使用同一套组件底层状态和交互逻辑,然后在UI层和细节上展开品牌和场景定制。所以,以上都是关于headless的设计理念。通过HeadlessUI,我们可以快速复用组件的状态和交互逻辑,完全自定义布局和样式。另外,HeadlessUI是组件库设计的一种新思路,也是未来组件库的必然趋势。对于前端同学来说,学会理解也尤为重要。值得一提的是,在日常开发中,我们也可以尝试借鉴这种思路,提取出通用的状态逻辑,方便复用,帮助我们在日常开发中提高效率。例如:普通过滤、分页请求列表数据逻辑等;甚至,我们还可以将业务逻辑从UI交互中分离出来,比如:在多端场景(WebPC、小程序、RN)复用同一套业务逻辑代码,实现业务逻辑的复用和统一,从而大大提高我们的生产力。