本文首发于微信公众号:大迁世界, 我的微信:qq449245884,我会第一时间和你分享前端行业趋势,学习途径等等。
更多开源作品请看 GitHub https://github.com/qq449245884/xiaozhi ,包含一线大厂面试完整考点、资料以及我的系列文章。
当把组件从设计转化为开发时,常常会发现一些属性与内容有关,而与组件本身无关。这种考虑周到的组件设计方法导致了复杂的属性、更陡峭的学习曲线和最终的技术债务。然而,避免这些陷阱的关键是自私或自我利益为中心的组件设计。
在开发新功能时,是什么决定了现有组件是否可行?当一个组件不能使用时,这究竟意味着什么?
该组件在功能上是否没有做它所期望的事情,比如一个标签系统没有切换到正确的面板?或者它太死板,不能支持设计的内容,比如一个在内容之后而不是之前有图标的按钮?或者是它太过预设和结构化,无法支持轻微的变体,比如一个一直有标题部分的模态,现在需要一个没有标题的变体?
这就是组件的生活。很多时候,它们是为了一个狭窄的目标而构建的,然后匆忙地为一个接一个的小变化进行扩展,直到不再可行。在这个时候,会创建一个新组件,技术债务增长,入职学习曲线变得更陡峭,代码库的可维护性变得更具挑战性。
这仅仅是组件不可避免的生命周期吗?还是这种情况可以避免?最重要的是,如果可以避免,怎么做?
自私。或者说,自利。更好的说法可能是两者兼而有之。
很多时候,组件过于体贴。它们对彼此太体贴了,尤其是对它们自己的内容太体贴了。为了创建能够随着产品扩展的组件,关键是自私地关注自己的利益——冷酷、自恋、世界环绕着我旋转的自私。
本文并不打算解决自利和自私之间几百年的争论。坦白说,我没有资格参与任何哲学辩论。然而,本文要做的是证明构建自私组件对其他组件、设计师、开发者和使用你内容的人来说是最有利的。事实上,自私的组件在它们周围创造了如此多的好处,以至于你几乎可以说它们是无私的。
注意:本文中的所有代码示例和演示都将基于React和TypeScript。然而,这些概念和模式是与框架无关的。
考虑的迭代
也许,展示一个体贴的组件的最好方式是通过走过一个组件的生命周期。我们将能够看到它们是如何开始时很小,功能很强,但一旦设计发展起来就会变得很笨重。每一次迭代都会使组件进一步陷入困境,直到产品的设计和需求超过了组件本身的能力。
让我们考虑一下谦虚的Button组件。它具有欺骗性的复杂性,而且经常被困在考虑模式中,因此,是一个很好的工作实例。
迭代1
虽然这些样本设计相当简陋,比如没有显示各种:hover
、:focus
和 :disabled
状态,但它们确实展示了一个有两种颜色主题的简单按钮。
乍一看,所产生的Button
组件有可能和设计一样是赤裸裸的。
// 首先,从React扩展原生HTML按钮属性,如onClick和disabled。
type ButtonProps = React.ComponentPropsWithoutRef & {
text: string;
theme: 'primary' | 'secondary';
}
有可能,甚至有可能,我们都见过这样的一个Button
组件。也许我们甚至自己也做过这样的一个。有些名字可能不一样,但 props 或Button的API大致上是一样的。
为了满足设计的要求,Button 为 theme
和 text
定义了 props 。这第一个迭代工作,满足了设计和产品的当前需求。
然而,设计和产品的当前需求很少是最终需求。当下次设计迭代时,添加到购物车的按钮现在需要一个图标。
迭代2
在验证了产品的用户界面后,决定在添加到购物车的按钮上增加一个图标,这将是有益的。不过,设计人员解释说,不是每个按钮都会包括一个图标。
回到我们的Button组件,它的 props 可以用一个可选的 icon
来扩展,该 props 映射到一个图标的名称,以便有条件地渲染。
type ButtonProps = {
theme: 'primary' | 'secondary';
text: string;
icon?: 'cart' | '...all-other-potential-icon-names';
}
呜呼!危机解除了。
有了新的 icon prop,Button 现在可以支持带或不带图标的变体。当然,这假定图标总是显示在文本的末尾,但出乎意料的是,在设计下一次迭代时,情况并非如此。
迭代3
以前的Button组件的实现包括文本末尾的图标,但新的设计要求图标可以选择放在文本的开头。单一的 icon prop 将不再适合最新设计要求的需要。
有几个不同的方向可以用来满足这个新产品的要求。也许可以给Button添加一个iconPosition
prop 。但如果需要在两边都有一个图标呢?也许我们的Button组件可以领先于这个假设的要求,对 prop 做一些改变。
单一的 icon prop 将不再适合产品的需要,所以它被移除。取而代之的是两个新 prop,iconAtStart
和iconAtEnd
。
type ButtonProps = {
theme: 'primary' | 'secondary' | 'tertiary';
text: string;
iconAtStart?: 'cart' | '...all-other-potential-icon-names';
iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
}
在重构代码库中Button的现有用途以使用新的 props ,另一个危机被避免了。现在,Button有了一些灵活性。所有这些都是硬编码的,并被包装在组件本身的条件中,但可以肯定的是,UI不知道的东西不会伤害它。
到目前为止,Button图标一直是与文本相同的颜色。这似乎是合理的,也是一个可靠的默认值,但让我们通过定义一个具有对比色的图标的变体来给这个运转良好的组件带来麻烦。
迭代4
为了提供一种反馈感,这个确认用户界面阶段被设计为在物品被成功添加到购物车时临时显示。
也许这个时候,开发团队会选择对产品需求进行反击。但尽管如此,还是决定继续推进为Button图标提供颜色灵活性。
同样,可以采取多种方法来解决这个问题。也许一个iconClassName
prop 被传递到 Button中,以便对图标的外观有更好的控制。但是,还有其他的产品开发重点,因此,只能做一个快速修复。
因此,一个 iconColor
prop 被添加到 Butto n中。
type ButtonProps = {
theme: 'primary' | 'secondary' | 'tertiary';
text: string;
iconAtStart?: 'cart' | '...all-other-potential-icon-names';
iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
iconColor?: 'green' | '...other-theme-color-names';
}
随着快速修复的到位,Button图标现在可以采用与文本不同的风格。UI可以提供所设计的确认,而产品可以再次向前推进。
当然,随着产品要求的不断增长和扩大,他们的设计也在不断发展。
迭代5
在最新的设计中,Button现在必须只用一个图标来使用。这可以用几种不同的方法来完成,然而,所有这些方法都需要进行一定程度的重构。
也许一个新的IconButton
组件被创建,将所有其他的按钮逻辑和样式重复到两个地方。或者,这些逻辑和样式被集中起来,在两个组件中共享。然而,在这个例子中,开发团队决定将所有的变体放在同一个Button组件中。
相反, text prop 被标记为可选。这可以像在 props 中标记为可选一样快速,但如果有任何期望文本存在的逻辑,可能需要额外的重构。
但是问题来了,如果Button只有一个图标,应该使用哪个图标道具?iconAtStart
和iconAtEnd
都没有适当地描述Button。最终,我们决定把原来的图标道具带回来,用于仅有图标的变体。
type ButtonProps = {
theme: 'primary' | 'secondary' | 'tertiary';
iconAtStart?: 'cart' | '...all-other-potential-icon-names';
iconAtEnd?: 'cart' | '...all-other-potential-icon-names';
iconColor?: 'green' | '...other-theme-color-names';
icon?: 'cart' | '...all-other-potential-icon-names';
text?: string;
}
现在,Button 的API越来越令人困惑。也许在组件中留有一些注释,以解释何时和何时不使用特定的 prop,但学习曲线越来越陡峭,出错的可能性也越来越大。
例如,如果不给 ButtonProps
类型增加巨大的复杂性,就无法阻止一个人同时使用 icon 和 text prop。这可能会破坏用户界面,或者在Button组件本身中用更复杂的条件来解决。此外, icon
prop 也可以与iconAtStart
和IconAtEnd
prop 中的一个或两个一起使用。同样,这可能会破坏用户界面,或者在组件内用更多的条件层来解决。
我们心爱的Button在这一点上已经变得相当难以管理了。希望产品已经达到一个稳定点,不会再有新的变化或要求发生。永远。
迭代6
这么说来,永远不会有任何变化了。🤦
Button
的下一个也是最后一个迭代是传说中压垮骆驼的那根稻草。在添加到购物车的按钮中,如果当前物品已经在购物车中,我们想在按钮上显示其中的数量。从表面上看,这是一个直接的变化,即动态地建立 text
prop 字符串。但是这个组件被打破了,因为当前商品数量需要一个不同的字体重量和下划线。因为Button只接受纯文本字符串,没有其他子元素,所以这个组件不再工作。
这个设计如果是第二次迭代的话,会不会导致按钮失效呢?也许不会。那时组件和代码库都还很年轻。但是到目前为止,代码库已经增长了很多,要为这个需求进行重构简直就像是攀登高峰。
这时可能会发生以下事情之一。
- 做一个更大的重构,把Button从一个 text prop 移到接受 children 或接受一个组件或标记作为
text
。 - 该 Button 被分割成一个单独的 AddToCart 按钮,有一个更严格的 API,专门用于这一个用例。这也是将任何按钮的逻辑和样式复制到多个地方,或者将它们提取到一个集中的文件中,以便到处共享。
- 按钮被弃用,并创建了一个 ButtonNew 组件,分裂了代码库,引入了技术债务,并增加了入职学习曲线。
两种结果都不理想。
那么,”按钮”组件在哪里出了问题?
分享是一种损害
HTML button 元素的职责究竟是什么?缩小这个答案将照亮之前Button组件所面临的问题。
原生的 HTML button元素的职责不过如此:
- 显示,没有意见,无论什么内容被传入它。
- 处理本地功能和属性,如
onClick
和disabled
。
是的,每个浏览器对按钮元素的外观和显示内容都有自己的版本,但CSS重置通常被用来剥夺这些意见。因此,按钮元素归根结底只是一个用于触发事件的功能性容器而已。
对按钮内的任何内容进行格式化不是按钮的责任,而是内容本身的责任。按钮不应该关心。按钮不应该分担对其内容的责任。
体贴的组件设计的核心问题是,组件 prop 定义了内容而不是组件本身。
在以前的 Button 组件中,第一个主要限制是 text
prop 。从第一次迭代开始,就对Button的内容进行了限制。虽然 text prop 符合那个阶段的设计,但它立即偏离了本地HTML按钮的两个核心职责。它立即迫使Button意识到并对其内容负责。
在接下来的迭代中,图标被引入。虽然在Button中加入一个有条件的图标似乎很合理,但这样做也偏离了按钮的核心职责。这样做限制了该组件的使用情况。在后来的迭代中,图标需要在不同的位置可用,而Button的 prop 也被迫扩展到图标的样式。
当组件对它所显示的内容负责时,它需要一个能适应所有内容变化的API。最终,这个API将被打破,因为内容将永远永远地改变。
介绍一下团队中的我#。
在所有团队运动中都有一句格言:”团队中没有’我'”。虽然这种心态很高尚,但一些最伟大的个人运动员却体现了其他想法。
迈克尔-乔丹用他自己的观点做出了著名的回应:”胜利中有一个’我'”。已故的科比-布莱恩特也有类似的想法,”[团队]里有一个’M-E'”。
我们最初的Button组件是一个团队成员。它分担了其内容的责任,直到它达到废弃的地步。按钮如何通过体现 “团队中的M-E “的态度来避免这种限制?
我,我自己,还有UI
当组件对它所显示的内容负责时,它就会崩溃,因为内容将永远永远地改变。
一个自私的组件设计方法会如何改变我们最初的按钮?
牢记HTML按钮元素的两个核心职责,我们的Button组件的结构会立即发生变化。
// 首先,从React扩展原生HTML按钮属性,如onClick和disabled
type ButtonProps = React.ComponentPropsWithoutRef & {
theme: 'primary' | 'secondary' | 'tertiary';
}
通过去掉原来的 text
prop 来代替无限的 children
,Button能够与它的核心职责保持一致。现在,Button可以作为一个触发事件的容器而已。
通过将Button转移到支持子内容的本地方法,不再需要各种与图标相关的道具。现在,一个图标可以在Button的任何地方呈现,无论其大小和颜色如何。也许各种与图标相关的道具可以被提取到他们自己的自私的 Icon
组件中。
随着特定内容的 prop 从 Button 中移除,它现在可以做所有自私的角色最擅长的事情,即思考自己。
// First, extend native HTML button attributes like onClick and disabled from React.
type ButtonProps = React.ComponentPropsWithoutRef & {
size: 'sm' | 'md' | 'lg';
theme: 'primary' | 'secondary' | 'tertiary';
variant: 'ghost' | 'solid' | 'outline' | 'link'
}
有了专门针对自身的API和独立的内容,Button现在是一个可维护的组件。自身的 props 使学习曲线最小化和直观化,同时为各种Button的使用案例保留了极大的灵活性。
按钮图标现在可以放置在内容的两端:
或者,一个Button可以只有一个图标:
然而,一个产品可能会随着时间的推移而演变,而自私的组件设计可以提高与之一起演变的能力。让我们超越Button,进入自私的组件设计的基石。
自私设计的关键
与创造一个虚构的人物时一样,最好是向读者展示,而不是告诉他们,他们是自私的。通过阅读人物的思想和行动,可以了解他们的个性和特征。组件设计也可以采取同样的方法。
但是,我们究竟如何在一个组件的设计和使用中表明它是自私的?
HTML驱动组件设计
很多时候,组件是作为本地HTML元素的直接抽象而构建的,比如 button
或 img
。在这种情况下,让本地HTML元素来驱动组件的设计。
具体来说,如果本地HTML元素接受子元素,那么抽象的组件也应该接受。一个组件的每一个方面如果偏离了它的原生元素,就必须重新学习。
当我们最初的Button组件因为不支持子内容而偏离了按钮元素的原生行为时,它不仅变得僵硬,而且需要转变思维模式才能使用该组件。
在HTML元素的结构和定义方面,已经投入了大量的时间和精力。轮子不需要每次都被重新发明。
children 自食其力
如果你读过《蝇王》,你就知道当一群孩子被迫自食其力时,会有多危险。然而,在自私的组件设计案例中,我们要做的正是这样。
正如我们最初的Button组件所显示的那样,它越是试图对其内容进行样式设计,它就越是僵硬和复杂。当我们去掉这个责任时,这个组件就能做得更多,但却少了很多。
许多元素只不过是语义上的容器而已。我们并不经常期望一个章节元素能够为其内容提供样式。一个按钮元素只是一个非常特殊的语义容器类型。当把它抽象为一个组件时,同样的方法可以适用。
组件是单一的重点
把自私的组件设计想象成安排一堆糟糕的第一次约会。一个组件的 prop 就像完全以他们和他们的直接责任为中心的对话。
我看起来怎么样?
prop 需要满足组件的自我要求。在我们重构的Button例子中,我们用大小、主题和变体等 prop做到了这一点。
我在做什么?
一个组件应该只对它,而且是它自己正在做的事情感兴趣。同样,在我们重构的Button组件中,我们用onClick
prop来做这个。就Button而言,如果在其内容的某个地方有另一个点击事件,那是内容的问题。按钮并不关心。
我的下一站是什么时候,在哪里?
任何喷射性的旅行者都会很快谈论他们的下一个目的地。对于像模态、抽屉和工具提示这样的组件来说,它们何时何地也同样重要。像这样的组件并不总是在DOM中呈现的。这意味着,除了知道它们的外观和作用之外,它们还需要知道何时何地。换句话说,这可以用isShown
和position
这样的props来描述。
构图为王
一些组件,如模版和抽屉,往往可以包含不同的布局变化。例如,有些模版会显示一个标题栏,而其他模版则没有。一些抽屉可能有一个带有行动呼吁的页脚。其他的可能根本没有页脚。
与其在单个模态或抽屉组件中用条件props (如hasHeader
或showFooter
)定义每个布局,不如将单个组件分解成多个可组合的子组件。
...
...
...
...
通过使用组件组合,每个单独的组件可以像它想的那样自私,只在需要的时候和地方使用。这样可以保持根组件的API干净,并且可以将许多 prop 转移到它们特定的子组件上。
当回顾我们的Button组件的演变时,也许自私的设计的关键是有意义的。然而,让我们再把它们应用到另一个普遍存在问题的组件–模态。
对于这个例子,我们在三个不同的模态布局中得到了预见性的好处。这将有助于引导我们Modal的方向,同时沿途应用自私设计的每个关键。
首先,让我们回顾一下我们的心理模型,并分解每个设计的布局。
在 “Edit Profile”模式中,有定义的页眉、主页和页脚部分。也有一个关闭按钮。在Upload Successful 中,有一个修改过的页眉,没有关闭按钮和一个类似英雄的图像。页脚的按钮也被拉长了。最后,在 Friends 模态中,关闭按钮返回,但现在内容区可以滚动,而且没有页脚。
那么,我们学到了什么?
我们了解到,页眉、主页和页脚部分是可以互换的。它们可能存在于任何给定的视图中,也可能不存在。我们还了解到,关闭按钮的功能是独立的,不与任何特定的布局或部分相联系。
因为我们的Modal可以由可互换的布局和安排组成,这就是我们采取可组合的子组件方法的标志。这将使我们能够根据需要在模态中插入和播放部件。
这种方法允许我们非常狭隘地定义我们的根Modal组件的职责。
有条件地以任何内容布局的组合进行渲染。
这就是了。只要我们的Modal只是一个有条件渲染的容器,它就永远不需要关心或对其内容负责。
随着我们的模态的核心职责被定义,以及可组合的子组件方法被决定,让我们来分解每个可组合的部分和它的作用。
组成部分 | 角色 |
---|---|
这是整个 Modal 组件的入口点。这个容器负责何时何地渲染,模态的外观,以及它所做的事情,比如处理可访问性的考虑。 | |
一个可互换的 Modal 子组件,只有在需要的时候才可以包含。这个组件的工作方式类似于我们重构的 Button 组件。它将负责它的外观,显示的位置,以及它的作用。 | |
标题部分将是本地HTML标题元素的一个抽象。它只不过是一个语义容器,用于显示任何内容,如标题或图片。 | |
主部分将是本地HTML主元素的一个抽象。它只不过是任何内容的一个语义容器而已。 | |
页脚部分将是本地HTML页脚元素的一个抽象化。它只不过是任何内容的一个语义容器而已。 |
有了每个组件及其角色的定义,我们可以开始创建道具来支持这些角色和责任。
Modal
我们定义了Modal的基本职责,即知道何时有条件地渲染。这可以通过isShown
这样的 prop 来实现。因此,我们可以使用这些 prop ,只要它是 true
`,Modal和它的内容就会渲染。
type ModalProps = {
isShown: boolean;
}
...
任何造型和定位都可以直接用CSS在Modal组件中完成。目前不需要创建特定的 prop。
Modal.CloseButton
鉴于我们之前重构的Button
组件,我们知道CloseButton
应该如何工作。我们甚至可以用我们的Button来构建我们的CloseButton
组件。
import { Button, ButtonProps } from 'components/Button';
export function CloseButton({ onClick, ...props }: ButtonProps) {
return (
)
}
Modal.Header, Modal.Main, Modal.Footer
每个单独的布局部分,Modal.Header
、Modal.Main
和Modal.Footer
,都可以从它们的HTML等价物,即header
、main
和footer
中获取方向。这些元素中的每一个都支持子内容的任何变化,因此,我们的组件也会这样做。
不需要特殊的 prop。它们只是作为语义容器。
...
...
...
有了我们的 Modal组件和它的子组件的定义,让我们看看它们是如何被互换使用来创建这三种设计的。
注意:完整的标记和样式没有显示出来,以便不影响核心的收获。
EDIT PROFILE MODAL
在Edit Profile模态中,我们使用了每个Modal
组件。然而,每一个都只是作为一个容器,它的样式和位置都是自己的。这就是为什么我们没有为它们包含一个className
prop。任何内容的样式都应该由内容本身来处理,而不是我们的容器组件。
Edit Profile
...
UPLOAD SUCCESSFUL MODAL
像前面的例子一样,Upload Successful模态使用其组件作为无意见的容器。内容的样式是由内容本身处理的。也许这意味着按钮可以被modal-button-wrapper
类拉伸,或者我们可以给Button组件添加一个”我看起来怎么样?”的道具,比如isFullWidth
,以获得更宽或全宽的尺寸。
Upload Successful
...
...
FRIENDS MODAL
最后,我们的Friendsmodal取消了Modal.Footer部分。在这里,在Modal.Main上定义溢出样式可能很诱人,但这是将容器的责任扩展到它的内容。相反,处理这些样式在modal-friends-wrapper类中更合适。
AngusMcSix's Friends
...
...
...
总结
本文对组件设计中的一个重要概念——自私性进行了探讨。自私性(Selfishness)在组件设计中是一种思维方式,意味着每个组件只关心其自身的功能和样式,而不关心其他组件。该文章认为,自私性可以帮助开发者创建更高效、易于维护的组件。
文章阐述了以下四个实践自私性的方法:
单一职责原则:组件应该有一个明确的功能,并仅关注该功能。这使组件更容易理解、测试和复用。
避免外部依赖:组件应该减少对外部资源的依赖,这有助于提高组件的独立性和复用性。
封装样式:组件的样式应该内部定义,避免受到外部样式影响。这样做可以确保组件在不同的环境中保持一致性。
明确接口:组件应该具有清晰、明确的接口,以便其他开发者能够容易地了解和使用组件。
作者强调,自私性并不意味着开发者应该孤立地工作,而是鼓励他们关注组件本身,从而提高组件的质量。通过遵循上述原则,开发者可以创建更加健壮、可维护和可扩展的组件,为整个项目带来长远的好处。
代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
原文:https://www.smashingmagazine.com/2023/01/key-good-component-d…
交流
有梦想,有干货,微信搜索 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net