查看原文
其他

大型项目组件目录设计实践方案

小懒 FED实验室 2024-02-12
关注下方公众号,获取更多技术实践系列文章

在开发大型、复杂项目时,你是不是经常遇到组件目录结构设计问题,是扁平化、模块化,还是当前流行的像 Next.js 一样的组件目录?本文将从通用性的组织方式开始介绍,然后介绍 Next.js 组件目录设计,最后深入探讨推荐的组件树目录设计方案。

1.扁平化组件目录结构

对于大多数前端开发者,首先想到的可能是将组件组织到语义正确的目录中去,即我们常说的语义化,比如下面这种:

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

components/
  layout/
    Layout.tsx
    Heading.tsx
    Footer.tsx
  common/
    Heading.tsx
    BoxContainer.tsx

这种结构设计,在小型项目中看着还比较直观,但在中大型项目中,随着页面的增多,不同页面间的组件复用率变高,会存在一下问题:

问题一:命名困难

作为开发人员,开发过程中遇到最多的可能就是文件、目录、样式类名的命名问题,你需要尝试为每个目录创建好的、语义化的名称和分离,如 layoutcontainersheadings 等等。

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

你经常会想说:"这样吧,我把这个移到 common 目录下"。对于你想要实现的目标来说,建立 common 目录是一种反模式,但在这种结构下,你很容易就会被它牵着鼻子走。而且,当你的应用程序变得足够大时,你可能不得不开始考虑创建另一级目录来保持事物的有序性。这就需要创建更多的名称,增加了资源库用户的认知负荷。最终,这种方法无法很好地扩展。

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

以前,试图浏览 repo 的人最初会试图通过每个组件的名称来了解它们的作用,以及它们之间的关系。

现在,他们还必须了解您创建的目录名称,如果这些名称在语义上不是一个整体,可能会让他们更加困惑。

2.类 Next.js 框架组件目录结构

一个简化的前端应用程序目录结构可能长这样:

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

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

从上述简单的应用结构中,我们很难了解这些组件是如何相互作用的。

例如,你可能会猜测 Layout.tsx 会导入 Footer.tsxHeader.tsx,而后者又会导入 BoxContainer.tsx。但单单从文件结构中还看不出这一点。

更糟糕的是,随着应用程序的增长,组件列表会变得越来越难以推断它们之间的联系。

3.更好的设计:组件树模式

采用这种方法时,你不必费尽心思用不同的名称来对组件组进行分类,而应将重点放在为组件取一个好名字上,并隐含地解释它们由哪些部分组成。

组件导入规则:

  • 可向上导入,但其父级组件除外
  • 可以导入同级组件
  • 不能导入同级组件的子组件
  • 不能导入父代

组件树结构示例:

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 中提取一些实用功能,因为这个文件有点大,你想你可以从中分离出一些逻辑,而不是分离出更多的用户界面。

虽然你可以创建一个 utils/ 目录,但这将迫使你为任何你想放置实用功能的文件取一个名字。

取而代之的是使用文件后缀,如 Footer.utils.tsxFooter.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 及其内部组件使用(再次向上导入)。

组件树模式的不同意见:

"组件目录太多了”,第一次看到这种结构时,大多数人的第一反应都是这样。是的,有很多 "组件" 目录。但当我与团队一起确定项目结构时,我总是强调清晰比优雅更重要。定义版本库成功与否的方法之一,就是看高级和初级开发人员如何看待清晰度,而我发现组件树总是有助于实现这一目标。

虽然 import … from ./MyComponent/MyComponent.tsx 看起来并不美观,但它通过直接指明组件的来源而带来的清晰度更为重要。

关于导入字符串,这些都是给开发人员造成认知负担的例子。

  • 使用导入别名,如 import ... from 'common/components',会给开发人员带来精神负担
  • 到处都有 index.ts 文件,只需写入 import ... from './MyComponent' 即可。对于按文件搜索的开发人员来说,找到正确的文件可能要花费更多时间。

复杂场景扁平化 vs 组件树目录结构:

有了 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

请记住,这是一个没有任何测试文件、实用程序文件或类似文件的示例。对于组件树结构,你可以在组件目录中添加后缀为实用程序或测试文件。至于扁平结构,你很可能需要创建一个单独的实用工具目录,以应付已经相当繁重的认知负荷。

总结

如果你正在进行大型项目实践,可以尝试使用组件树结构。你会发现它是如此的直观和高效,以至于你再也无法回到其他更复杂的结构中去,因为它们无法简化组件管理。

‍大家都在看

继续滑动看下一个

大型项目组件目录设计实践方案

小懒 FED实验室
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存