在过去几年中,现代网站的复杂性显着增加。对高质量、行业标准设计的需求不断增加,进一步加剧了前端开发人员面临的挑战。
今天,即使是前端应用程序也需要一些架构考虑来简化开发过程。在我之前的文章中,我分享了我在从事业余项目时在前端应用程序中实施干净架构方法的经验。
在本文中,我的目标是根据我在同一项目中的经验,更深入地研究原子设计方法。我将讨论它的优点和缺点,并评估它在不同场景中的用处。
首先,让我们探讨一下设计系统的概念。设计系统是可重用组件、指南和原则的综合集合,使团队能够跨多个平台设计和开发一致的用户界面。
它们充当设计师和开发人员的单一事实来源,确保产品的视觉和功能方面符合并坚持既定的品牌标识。如果您有兴趣探索设计系统实现的示例,请考虑检查以下内容:
如果您想更深入地研究设计系统的主题,我建议您查看这篇文章。它详细描述了这个主题,在这项工作的范围内我们不需要的细节。
建立在设计系统的基础上,原子设计是一种简化可重用组件和指南的组织和结构的方法。 Atomic Design 由 Brad Frost 构思,灵感来自化学,因为它将用户界面解构为最基本的构建块,并将它们重新组合成更复杂的结构。
这是一张说明与化学类比的图像:
化学反应由化学方程式表示,这些方程式通常显示原子元素如何结合在一起形成分子。在上面的例子中,我们看到氢和氧如何结合在一起形成水分子。
本质上,原子设计是设计系统的自然演变,提供了一种系统的方法来创建灵活和可扩展的组件。通过应用原子设计原则,团队可以更有效地管理他们的设计系统,因为这种方法的模块化特性使得维护、更新和扩展系统内的组件和模式变得更加容易。
如果您担心这听起来很复杂,请不要担心。在接下来的部分中,我将使用我开发的应用程序中的真实示例演示如何应用这些原则,使其易于理解并在您自己的项目中实施。
原子设计将组件组织成五个不同的级别,每个级别都建立在前一个之上。让我们详细探讨这五个级别:
原子:用户界面最基本的构建块,原子代表单个 HTML 元素,例如按钮、输入字段和标题。它们是最小的功能单元,不能进一步分解。
分子:分子是通过将两个或多个原子结合成一个官能团而形成的。例如,搜索表单分子可能由搜索输入原子、按钮原子和标签原子组成。分子代表可以在项目中重复使用的简单组件。
有机体:有机体是更复杂的组成部分,由多个分子和/或原子组合而成。它们代表用户界面的不同部分,例如页眉、页脚或侧边栏。有机体有助于形成页面的整体布局和结构。
模板:模板本质上是使用有机体、分子和原子构建的页面布局。它们定义页面上组件的结构和排列,而不指定任何实际内容,作为各种内容场景的蓝图。
pages :页面是最终的、完全实现的模板实例,包含真实的内容和数据。它们代表用户最终会看到什么并与之交互,展示组件和布局如何适应不同的内容类型和用例。
为了对前端开发的原子设计有一个全面的了解,我开始了创建应用程序的旅程。在六个月的时间里,我在从事这个项目的过程中获得了宝贵的见解和经验。
因此,本文中提供的示例均来自我对该应用程序的亲身体验。为了保持透明度,所有示例均来自可公开访问的代码。
请记住,我将使用在React中编码的示例。如果您不熟悉这种语言,请不要担心——我的目的是说明原子设计的基本概念,而不是专注于代码的细节。
为了更好地了解我的存储库中的组件,您可以在以下目录下找到它们: /client/presentation
。在此位置,我创建了一个名为atoms
的新目录,以与原子设计方法保持一致的命名。这个新目录包含构建整个入职流程所需的所有小部分。
完整的原子列表如下:
atoms ├── box ├── button ├── card ├── card-body ├── card-footer ├── container ├── divider ├── flex ├── form-control ├── form-error-message ├── form-helper-text ├── form-label ├── heading ├── icon ├── input ├── list ├── list-icon ├── list-item ├── spinner ├── tab ├── tab-list ├── tab-panel ├── tab-panels ├── tabs └── text
这些原子名称您可能很熟悉,因为它们基于Chakra UI包。它们中的大多数已经包含了我的应用程序的默认匹配样式,因此在这个级别没有什么特别独特的东西可以描述。考虑到这一点,我们可以直接着手讨论molecules
。
在这个阶段,原子设计过程变得更加有趣,它的真正力量开始显现出来。虽然定义基本原子可能是一项耗时且单调的任务,但使用原子构建新组件变得更加有趣。
为了定义分子,我在/client/presentation
目录中创建了一个molecules
目录。所需分子的完整列表如下:
molecules ├── available-notion-database ├── full-screen-loader ├── input-control ├── onboarding-step-layout └── onboarding-tab-list
事实上,只有五个分子,我们就有足够的成分来实现我们的目标。重要的是要注意,这也是包含基于其他原子构建的共享布局的理想位置。例如, onboarding-step-layout
用于在入职流程的所有五个步骤中保持一致的外观。
其他组件如下:
available-notion-database :用于显示获取的用户的数据库详细信息(用户可能有多个数据库,因此我提供了在步骤 4 中选择一个的能力)。
该组件在 UI 上显示如下:
import { FC } from 'react'; import { Flex, Spinner } from '@presentation/atoms'; import { FullScreenLoaderProps } from './full-screen-loader.types'; export const FullScreenLoader: FC<FullScreenLoaderProps> = ({ children, ...restProps }): JSX.Element => ( <Flex alignItems="center" bg="gray.50" height="full" justifyContent="center" left={0} position="fixed" top={0} width="full" zIndex="9999" {...restProps} > <Spinner /> {children} </Flex> );
这里没有火箭科学。这只是已经定义的flex
和spinner
原子的组合。
input
原子的包装器,带有form-label
、 form-control
、 form-error-label
和spinner
以显示是否有一些后台操作正在发生。该组件在 UI 上显示如下:
现在准备好了更多的部分,我们可以继续在我们的设计难题中定义更大的块。
这部分是我创建负责显示入职流程每个步骤的每个组件的地方。
为了澄清事情,我将向您展示已创建生物体的列表:
organisms ├── onboarding-step-one ├── onboarding-step-two ├── onboarding-step-three ├── onboarding-step-four └── onboarding-step-five
我相信名称是不言自明的,不应有任何误解。为了说明我是如何将所有内容放在一起的,我将以一个步骤的代码为例。当然,如果你想查看更多,只需访问我的存储库。
export const OnboardingStepFour: FC<OnboardingStepFourProps> = ({ onBackButtonClick, onNextButtonClick, }): JSX.Element => { const { hasApiTokenData, isSetApiTokenLoading, setApiToken, setApiTokenError } = useSetApiToken(); const handleInputChange = debounce(async (event: ChangeEvent<HTMLInputElement>) => { const result = await setApiToken(event.target.value); if (result) { onNextButtonClick(); } }, 1000); return ( <OnboardingStepLayout subtitle="Paste your copied integration token below to validate your integration." title="Validate your integration" onBackButtonClick={onBackButtonClick} > <InputControl isRequired errorMessage={setApiTokenError || undefined} isDisabled={isSetApiTokenLoading || hasApiTokenData} isLoading={isSetApiTokenLoading} label="Integration token" name="integrationToken" placeholder="Your integration token" onChange={handleInputChange} /> </OnboardingStepLayout> ); };
此代码完全负责显示我的入职流程中的第四步。我相信您可能唯一担心的是在生物体中提出请求。这是可以接受的吗?没有一个放之四海而皆准的答案,我需要用“视情况而定”来回应这些担忧。这取决于你的结构。
如果在分子或有机体中包含 API 调用在您的应用程序上下文中有意义并且不会使组件过于复杂,那么它可能是一个可接受的解决方案。请注意不要让表示组件与数据获取或业务逻辑紧密耦合,因为这会使它们更难维护和测试。
在我的场景中,这个组件在一个地方使用,而在该场景中执行 API 调用的其他解决方案更加复杂,并且可能会产生比必要的更多的代码。
在这个阶段,重点是组件的结构和排列,而不是 UI 的更精细的细节。模板还有助于确定状态管理应位于何处,通常位于使用模板的页面组件中。
在提供的代码示例中,我们有一个用作模板Onboarding
组件:
import { FC } from 'react'; import { Flex, Heading, TabPanels, Tabs, Text } from '@presentation/atoms'; import { OnboardingTabList } from '@presentation/molecules'; import { OnboardingStepFive, OnboardingStepFour, OnboardingStepOne, OnboardingStepThree, OnboardingStepTwo, } from '@presentation/organisms'; import { OnboardingProps } from './onboarding.types'; export const Onboarding: FC<OnboardingProps> = ({ activeTabs, createNotionIntegrationTabRef, displayCreateNotionIntegrationTab, displaySelectNotionDatabaseTab, displayShareDatabaseIntegrationTab, displayValidateIntegrationTab, displayVerifyDatabaseTab, selectNotionDatabaseTabRef, shareDatabaseIntegrationTabRef, validateIntegrationTabRef, verifyDatabaseTabRef, }) => ( <Flex direction="column" overflowX="hidden" px={2} py={{ base: '20px', sm: '25px', md: '55px' }}> <Flex direction="column" textAlign="center"> <Heading color="gray.700" fontSize={{ base: 'xl', sm: '2xl', md: '3xl', lg: '4xl' }} fontWeight="bold" mb="8px" > Configure your Notion integration </Heading> <Text withBalancer color="gray.400" fontWeight="normal"> This information will let us know from which Notion database we should use to get your vocabulary. </Text> </Flex> <Tabs isLazy display="flex" flexDirection="column" mt={{ base: '10px', sm: '25px', md: '35px' }} variant="unstyled" > <OnboardingTabList activeTabs={activeTabs} createNotionIntegrationTabRef={createNotionIntegrationTabRef} selectNotionDatabaseTabRef={selectNotionDatabaseTabRef} shareDatabaseIntegrationTabRef={shareDatabaseIntegrationTabRef} validateIntegrationTabRef={validateIntegrationTabRef} verifyDatabaseTabRef={verifyDatabaseTabRef} /> <TabPanels maxW={{ md: '90%', lg: '100%' }} mt={{ base: '10px', md: '24px' }} mx="auto"> <OnboardingStepOne onNextButtonClick={displayCreateNotionIntegrationTab} /> <OnboardingStepTwo onBackButtonClick={displayVerifyDatabaseTab} onNextButtonClick={displayShareDatabaseIntegrationTab} /> <OnboardingStepThree onBackButtonClick={displayCreateNotionIntegrationTab} onNextButtonClick={displayValidateIntegrationTab} /> {activeTabs.validateIntegration ? ( <OnboardingStepFour onBackButtonClick={displayShareDatabaseIntegrationTab} onNextButtonClick={displaySelectNotionDatabaseTab} /> ) : null} {activeTabs.selectNotionDatabase ? ( <OnboardingStepFive onBackButtonClick={displayVerifyDatabaseTab} /> ) : null} </TabPanels> </Tabs> </Flex> );
此Onboarding
组件组装原子、分子和有机体以创建入职流程的布局。请注意,状态管理和选项卡导航逻辑已从该组件中分离出来。必要的状态和回调函数现在作为道具接收,允许更高级别的“页面”组件处理状态和数据管理。
这种关注点分离使模板专注于布局和结构,同时确保在适当的级别处理状态管理。
最后,我想将步骤 4 作为最终结果呈现:
在我们之前讨论的上下文中,“页面”组件使用Onboarding
模板并处理入职流程的状态管理。虽然此处未提供此特定页面组件的代码,但您可以在我的存储库中找到它。如前所述,页面组件的代码没有什么特别之处;它主要侧重于管理状态并将其传递给Onboarding
模板。
如果我们看一下原子设计在实践中是什么样子的。让我们深入探讨这种方法的优缺点。
虽然原子设计提供了许多明显的好处,例如模块化、可重用性和可维护性,但它也有一些缺点,值得在开始时考虑:
初始设置和复杂性:原子设计需要精心规划的结构和组织,这在初始设置时可能既耗时又具有挑战性。它还可能给您的代码库带来额外的复杂性,特别是对于可能不需要这种细粒度方法的小型项目。
学习曲线:对于刚接触原子设计的开发人员来说,该方法可能具有陡峭的学习曲线。它需要对不同级别以及它们如何组合在一起有扎实的理解,这对初学者来说可能是压倒性的。
开销:实现原子设计可能涉及创建大量小型专用组件。这会导致管理和维护这些组件的开销增加,尤其是当组件仅在一个特定上下文中使用时。
过度工程的风险:专注于创建可重用和模块化的组件,存在过度工程的潜在风险,开发人员可能会花费太多时间来改进单个组件,而不是专注于更广泛的应用程序。
沟通与协作:原子设计的成功取决于设计师、开发人员和其他利益相关者之间清晰的沟通与协作。未能建立共同语言或对方法的理解可能导致实施过程中的混乱和不一致。
然而,这种方法有其自身已经提到的优势。让我们更详细地讨论它们:
可扩展性:通过将设计分解为最基本的元素,构建组件的复杂性成为一项更易于管理的任务。虽然制作原子会带来一些挑战,但基于这些原子创建任何组件都非常有趣。
效率:重用原子、分子和有机体的能力显着减少了设计和开发新功能所花费的时间。一旦建立了基本组件,创建新界面就像组合现有元素一样简单。
一致性:直接来自上一点。由于在多个模板和页面中使用相同的原子、分子和生物体,因此用户界面保持统一,为用户提供无缝体验。
文档:原子设计本质上支持文档。基于原子的结构可以作为如何构建和使用组件的清晰、直观的指南。这对于入职新团队成员特别有帮助。
可维护性:原子设计的最大优势之一是它如何有助于设计系统的可维护性。通过将所有内容分解成原子部分,任何更改或更新都可以在原子级别进行,然后通过系统传播。例如,如果你决定改变一个按钮的颜色,你只需要在原子级别进行一次更改,它就会反映在使用这个按钮的所有分子、生物体和模板上。随着时间的推移,这大大简化了更新和维护设计系统的过程。
总之,虽然原子设计可能看起来像一把双刃剑——在初始设置和学习曲线方面有点令人生畏——但它的潜在好处非常值得最初的努力。请记住,即使是最强大的剑在熟练的骑士手中也是无害的!