大型项目组件目录设计实践方案
在开发大型、复杂项目时,你是不是经常遇到组件目录结构设计问题,是扁平化、模块化,还是当前流行的像 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
这种结构设计,在小型项目中看着还比较直观,但在中大型项目中,随着页面的增多,不同页面间的组件复用率变高,会存在一下问题:
问题一:命名困难
作为开发人员,开发过程中遇到最多的可能就是文件、目录、样式类名的命名问题,你需要尝试为每个目录创建好的、语义化的名称和分离,如 layout
、containers
、headings
等等。
问题在于,你需要为目录考虑更多的分类,而不仅仅是组件名称。
你经常会想说:"这样吧,我把这个移到 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.tsx
和 Header.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.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
及其内部组件使用(再次向上导入)。
组件树模式的不同意见:
"组件目录太多了”,第一次看到这种结构时,大多数人的第一反应都是这样。是的,有很多 "组件" 目录。但当我与团队一起确定项目结构时,我总是强调清晰比优雅更重要。定义版本库成功与否的方法之一,就是看高级和初级开发人员如何看待清晰度,而我发现组件树总是有助于实现这一目标。
虽然 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
请记住,这是一个没有任何测试文件、实用程序文件或类似文件的示例。对于组件树结构,你可以在组件目录中添加后缀为实用程序或测试文件。至于扁平结构,你很可能需要创建一个单独的实用工具目录,以应付已经相当繁重的认知负荷。
总结
如果你正在进行大型项目实践,可以尝试使用组件树结构。你会发现它是如此的直观和高效,以至于你再也无法回到其他更复杂的结构中去,因为它们无法简化组件管理。
大家都在看