组件设计之组合原则

前言 在过去的几个月里,我和客户团队在对一个组件库Atlaskit进行优化。表面上看起来这个优化工作包括两大部分:性能优化和结构重整。不过经过这几个月对十多个组件的重构之后,我们发现这两部分工作在很大程度上是同一件事的两个方面:好的设计往往可以带来更好的性能,反之亦然。 这是一个非常有趣的发现,我们在讨论性能优化的时候,一个经常被忽略的因素恰恰是软件本身的设计。我们会关注文件大小,是否会有多重渲染,甚至一些细节如CSS selector的优先级等等,但是很少为了性能而审视代码的设计。另一方面,如果一个组件写的不符合S.O.L.I.D原则,我们会认为它的可扩展性不够好,或者由于文件体量过大,且职责不清而变得难以维护,但是往往不会认为糟糕的设计会对性能造成影响(也可能是由于性能总是在实现已经完成之后才被注意到)。 为了更好的说明这个问题,以及如何在实践中修改我们的设计,使得代码更可能具有比较优秀的性能,我们可以一起讨论几个典型的例子。 头像组件Avatar 在Atlaskit比较早的一个版本中,头像Avatar组件有一个很方便的功能:如果给Avatar传入了name属性,则当鼠标悬停到头像时,头像下方会显示一个提示信息(Tooltip),内容为对应的name。 在实现中,Avatar使用了另一个组件Tooltip来完成这个功能: import Tooltip from "@atlaskit/tooltip"; const Avatar = (props) => { if (props.name) { return ( <Tooltip content={props.name}> <Circle> <img src={props.url} /> </Circle> </Tooltip> ); } return ( <Circle> <img src={props.url} /> </Circle> ); }; 这个功能本身并没有问题,不过当用户提出更多的需求后,我们就开始失去了对Avatar的控制。比如用户A希望鼠标悬停的时候,Tooltip可以显示在头像的上方。而用户B则希望可以定制Tooltip的背景色/字体/字号等等。 当然,我们可以开放一些新的参数给Avatar来实现这些需求,比如: <Avatar tooltipPosition="top" tooltipBackgroundColor="blue" tooltipColor="whitesmoke" />; 或者更进一步,开放一个选项对象: <Avatar tooltipProps={{ position: "top", backgroundColor: "blue", color: "whitesmoke", }} />; 然后在实现中我们将其透传给Tooltip组件。不过很快我们会发现这样的方式会带来一些问题: 由于Avatar依赖于Tooltip,打包后文件的尺寸会增加 如果用户需要以新的方式定制Tooltip,Avatar的接口也需要相应的更新 由于这个依赖,当Tooltip的API变化时,Avatar需要重新打包 而如果我们审视Avatar组件的话,会发现Tooltip对其核心功能(显示用户头像)来说,更像是起到了辅助作用,而并非不可或缺。比如,假设不使用Tooltip组件,我们可以把Avatar简化为: const Avatar = (props) => ( <Circle> <img src={props....

May 1, 2021 · 3 min · 邱俊涛 | Juntao Qiu

自动化重构 - jscodeshift

自动化重构(jscodeshift) 在这篇文章里我想要通过一些小例子来介绍使用jscodeshift来进行自动化重构的技术。具体来说,我想要介绍在一个组件库的开发和维护过程中,如何使用jscodeshift来自动修改公开的API接口,从而尽可能小的产生对组件用户的影响。 如果你们团队开发的组件被其消费者(组织内部或者外部)使用了,而这些代码又不在你的控制之内,那么这里讨论的技术和模式可能对你很有帮助。而如果你的日常工作更多的是使用组件库来开发应用程序,我希望这里的知识和技巧仍然对你有所启发,毕竟在软件系统中,我们往往都既是某些库的消费者,又同时是另外一些库的生产者。 从一个简单场景出发 设想这样一个场景,你发布了一个酷炫的组件库(fancylib),其中有一个按钮(Button)组件。这个Button的一个属性是当点击后处于加载中(loading)状态时现实一个表示加载中的小图标。 (图片来源:https://xd.adobe.com/ideas/process/ui-design/designing-interactive-buttons-states/) 在代码实现中,这个加载中状态被定义为了名为isInLoadingStatus公开prop。用户可以通过设置其值来控制Button的状态: import Button from '@fancylib/button'; const app = () => ( <Button isInLoadingStatus>Click me</Button> ) 一个实习生在某一天code review的时候提出了一个问题:在组件库中的其他地方,所有的boolean状态都是用一个单词来表示的,比如checked, disabled等。如果按照这个惯例,这里应该把isInLoadingStatus简化为loading。好主意! import Button from '@fancylib/button'; const app = () => ( <Button loading>Click me</Button> ) 假如所有用到Button的地方都在你的控制之内,字符串替换大约是一个快速且80%有效的方案。不过稍微分析一下,你就会发现简单的Shift+F6会遇到很多问题。 复杂情况 比如用户对其做了二次包装以适配更符合自己用户的使用习惯,这使得简单的全局字符串替换变成了不可能:: import Button as FancyButton from '@fancylib/button'; const MyEvenFancierButton = (props: FancyButtonProps) => ( const theme = { backgroundColor: "orangered", color: "white" }; <FancyButton {...props} theme={theme}>Click me</FancyButton> ); 除了这些问题之外,由于这是一个非常受欢迎的组件库,Button在很多(包括内部和外部的)产品中都有使用,你没有办法访问所有的用户代码,更没有办法让所有人都用手工的查找替换来做更新,你需要另寻出路。 你需要一个工具 – 一个可以读懂代码意图的工具 – 来帮助你做修改,而且整个过程最好可以自动化,比如通过执行一个脚本来完成。...

December 11, 2020 · 2 min · 邱俊涛 | Juntao Qiu