组件树

90 阅读6分钟

组件树,一种减少认知负荷的结构。今日前端早读课文章由@南城分享,公号:南城大前端授权。

正文从这开始~~

自很久以前遵循互联网上的建议以来,我一直采用了某种 “能工作就行” 的组件结构。

场景

让我们首先想象一个简化的前端应用程序目录结构,如下所示:

 public/
   some-image.jpg
 pages/
   index.tsx

 components/
   Heading.tsx
   Logo.tsx
   Layout.tsx
   BoxContainer.tsx
   Footer.tsx

问题所在

上面的简单应用程序结构很难解释这些组件之间是如何相互作用的。

例如,您可能会猜测 Layout.tsx 导入 Footer.tsx 和 Header.tsx,而这又可能导入 BoxContainer.tsx。但这仅仅从文件结构上是不清楚的。

更糟糕的是,随着应用程序的增长,组件列表将变得越来越难以推断它们是如何依赖的。

简单方法:扁平组件结构

通常首先想到的是将组件组织到语义正确的目录中。

下面是这种方法的典型结果:

 public/
   some-image.jpg
 pages/
   index.tsx

 components/
   layout/
     Layout.tsx
     Heading.tsx
     Footer.tsx
   common/
     Heading.tsx
     BoxContainer.tsx
问题 1:很难扩展好名字

作为一个开发人员,您会尝试为每个目录创建好的名称和分类,如 containers、headings 等。

问题是您需要为目录考虑更多的分类,而不仅仅是组件名称。

你经常会忍不住说,“我就把这个移到公共目录吧。” 拥有 common 目录是你所追求的目标相悖的反模式,但是在这种结构下,很容易被吸引进入其中。

而且当应用程序变得足够大时,您可能不得不开始考虑创建另一级目录来保持内容的组织性。

这需要创建更多的名称,增加了存储用户的认知负荷。最终这种方法无法很好地扩展。

问题 2:目录名称的认知负荷增加

在此之前,那些浏览代码库的人首先会通过组件的名称以及它们之间的关系来初步了解每个组件的功能。

现在他们还需要理解你创建的目录名称,如果这些名称在语义上不符合整体,这可能会使他们更加困惑。

更好的方法:组件树模式

使用这种方法,您的重点是拥有命名良好的组件,这些组件隐式地解释了它们的组成,而不用特意对具有不同名称的组件组进行分类。

组件导入规则

  • 可以向上导入,除了它自己的父级
  • 可以导入同级
  • 无法导入同级组件
  • 无法导入其父级
 public/
   some-image.jpg
 pages/
   index.tsx

 components/
   Layout/
     components/
       Heading/
         components/
           Logo.tsx
           Menu.tsx
         Heading.tsx
       CopyrightIcon.tsx
       Footer.tsx
     Layout.tsx
   BoxContainer.tsx

让我们展示 Footer.tsx 的内容,并使用上面列出的规则作为示例:

 // components/Layout/components/Footer.tsx

 // Can import upwards, except its own parent
 import { BoxContainer } from '../../BoxContainer.tsx';
 // Can import siblings
 import { CopyrightIcon } from './CopyrightIcon.tsx';

 // WRONG: Cannot import sibling's components
 // import { Menu } from './Heading/components/Menu.tsx';
 // WRONG: Cannot import its parent
 // import { Layout } from '../Layout.tsx';

 export const Footer = () => (
   <BoxContainer>
     <CopyrightIcon />
     <p>All rights reserved, etc.</p>
   </BoxContainer>
 )
优点 1:明显的子组件关系

组件树模式消除了猜测;组件之间的关系立即变得清晰明了。例如,Menu.tsx 作为 Heading.tsx 的内部依赖被整齐地嵌套在其中。

同样清晰的是 Menu.tsx 没有被其他任何组件使用,这有助于您在日常开发任务中清理代码时尽早忽略它。

优点 2:可重用性的定义更加细致入微

在简单的方法中,组件被分为 “常见” 和 “非常见” 两种。考虑到可重用性,组件树有助于避免这种无效的二元思维。

 components/
   Layout/
     components/
       Heading/
         components/
         - Logo.tsx
           Menu.tsx
         Heading.tsx
     + Logo.tsx
       CopyrightIcon.tsx
       Footer.tsx
     Layout.tsx
   BoxContainer.tsx

在上面的例子中,如果 Logo.tsx 对于更多的组件变得必要,而不仅仅是 Menu.tsx,我们可以简单地将其上移一级。对于 BoxContainer.tsx 来说,它可能没有足够的可重用性(或 “通用性”),但在 Layout.tsx 组件的上下文中它是足够可重用的。

优点 #3:尽量减少命名

由于您有组件树,因此不需要将目录名分类在组件名之上。组件名称是分类,当您看到组件由哪些内部组件组成时,为组件确定好的名称也会更容易。

额外的好处:从组件中提取代码到单独的文件中,而无需考虑名称

现在我们考虑一种情况,您希望从 Footer.tsx 中提取一些实用程序函数,因为文件变得有点大,并且您认为可以从中分解一些逻辑,而不是分解更多的 UI。

虽然你可以创建一个 utils / 目录,但这会迫使你选择一个文件名来放置你的实用函数。

相反,选择使用文件后缀,如 Footer.utils.tsx 或 Footer.test.tsx。

 components/
   Layout/
     components/
       Heading/
         components/
           Logo.tsx
           Menu.tsx
         Heading.tsx
       CopyrightIcon.tsx
     + Footer.utils.tsx
       Footer.tsx
     Layout.tsx
   BoxContainer.tsx

这样你就不必去想一个很合适的名字,如 emailFormatters.ts 或非常模糊的东西,如 helpers.ts。避免命名带来的认知负担,这些实用程序属于 Footer.tsx,可以由 Footer.tsx 及其内部组件使用(再次向上导入)。

【第3118期】原生“跨组件”通信方式

组件树的反驳观点

“太多的组件目录了”

第一次看到这个结构,这是大多数人的下意识反应。

是的,有很多 “组件” 目录。但当我与团队一起确定项目结构时,我总是强调清晰度的重要性。

我在一个代码库中衡量成功的方法之一是高级和初级开发人员对于清晰度的看法,而在这方面,我发现组件树总是对实现这个目标起到重要作用。

“呃:import … from ./MyComponent/MyComponent.tsx?

虽然 import … from ./MyComponent/MyComponent.tsx 可能看起来不漂亮,但它直接指示组件来自哪里带来的清晰度更重要。

关于导入字符串,以下是为开发人员增加认知负荷的示例。

使用像 import ... from 'common/components' 这样的导入别名对开发人员来说是一种精神负担
到处都有 index.ts 文件,只需要写 import ... from './MyComponent'。但对于按文件搜索的开发人员来说,找到正确的文件可能需要更多的时间。

最终比较:复杂场景

多亏了像 ChatGPT 这样的工具,为更复杂的场景测试这样的模式非常容易。

在解释了结构之后,我让 ChatGPT 在左列生成 “平面” 目录结构,在右边生成我称为 “组件树” 的结构。

 Flat Structure                      |  Component Trees
 ------------------------------------+---------------------------------------------------
 pages/                              |  pages/
   index.tsx                         |    index.tsx
   shop.tsx                          |    shop.tsx
   product/                          |    product/
     [slug].tsx                      |      [slug].tsx
   cart.tsx                          |    cart.tsx
   checkout.tsx                      |    checkout.tsx
   about.tsx                         |    about.tsx
   contact.tsx                       |    contact.tsx
   login.tsx                         |    login.tsx
   register.tsx                      |    register.tsx
   user/                             |    user/
     dashboard.tsx                   |      dashboard.tsx
     orders.tsx                      |      orders.tsx
     settings.tsx                    |      settings.tsx
                                     |
 components/                         |  components/
   layout/                           |    Layout/
     Layout.tsx                      |      components/
     Header.tsx                      |        Header/
     Footer.tsx                      |          components/
     Sidebar.tsx                     |            Logo.tsx
     Breadcrumb.tsx                  |            NavigationMenu.tsx
   common/                           |            SearchBar.tsx
     Button.tsx                      |            UserIcon.tsx
     Input.tsx                       |            CartIcon.tsx
     Modal.tsx                       |          Header.tsx
     Spinner.tsx                     |        Footer/
     Alert.tsx                       |          components/
   product/                          |            SocialMediaIcons.tsx
     ProductCard.tsx                 |            CopyrightInfo.tsx
     ProductDetails.tsx              |          Footer.tsx
     ProductImage.tsx                |      Layout.tsx
     ProductTitle.tsx                |    BoxContainer.tsx
     ProductPrice.tsx                |    Button.tsx
     AddToCartButton.tsx             |    Input.tsx
   filters/                          |    Modal.tsx
     SearchFilter.tsx                |    Spinner.tsx
     SortFilter.tsx                  |    Alert.tsx
   cart/                             |    ProductCard/
     Cart.tsx                        |      components/
     CartItem.tsx                    |        ProductImage.tsx
     CartSummary.tsx                 |        ProductTitle.tsx
   checkout/                         |        ProductPrice.tsx
     CheckoutForm.tsx                |        AddToCartButton.tsx
     PaymentOptions.tsx              |      ProductCard.tsx
     OrderSummary.tsx                |    ProductDetails/
   user/                             |      components/
     UserProfile.tsx                 |        ProductSpecifications.tsx
     UserOrders.tsx                  |        ProductReviews.tsx
     LoginBox.tsx                    |        ProductReviewForm.tsx
     RegisterBox.tsx                 |      ProductDetails.tsx
   about/                            |    SearchFilter.tsx
     AboutContent.tsx                |    SortFilter.tsx
   contact/                          |    Cart/
     ContactForm.tsx                 |      components/
   review/                           |        CartItemList.tsx
     ProductReview.tsx               |        CartItem.tsx
     ProductReviewForm.tsx           |        CartSummary.tsx
   address/                          |      Cart.tsx
     ShippingAddress.tsx             |    CheckoutForm/
     BillingAddress.tsx              |      components/
   productInfo/                      |        PaymentDetails.tsx
     ProductSpecifications.tsx       |        BillingAddress.tsx
   cartInfo/                         |        ShippingAddress.tsx
     CartItemList.tsx                |      CheckoutForm.tsx
   userDetail/                       |    PaymentOptions.tsx
     UserSettings.tsx                |    OrderSummary.tsx
   icons/                            |    UserProfile/
     Logo.tsx                        |      components/
     SocialMediaIcons.tsx            |        UserOrders.tsx
     CartIcon.tsx                    |        UserSettings.tsx
     UserIcon.tsx                    |      UserProfile.tsx
                                     |    LoginBox.tsx
                                     |    RegisterBox.tsx
                                     |    AboutContent.tsx
                                     |    ContactForm.tsx

这是一个没有任何测试文件、实用程序文件或类似文件的示例。

对于组件树结构,您可以在组件目录中添加后缀为的实用程序或测试文件。