React 项目结构的规模:分解、层和层次结构
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--YOBuVY7B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/rq5u5fxsnhpqy5w9zoqw.png)
最初发表于https://www.developerway.com。该网站有更多这样的文章😉
...
只要 React 存在,如何“以正确的方式”构建 React 应用程序似乎就是最近的热门话题。 React 官方对此的看法是它“没有意见”。这很棒,它给了我们完全的自由去做我们想做的任何事情。而且也很糟糕。它导致了许多关于正确的 React 应用程序结构的根本不同和非常强烈的意见,即使是最有经验的开发人员有时也会感到迷茫、不知所措,并因此需要在黑暗的角落里哭泣。
当然,我对这个话题也有强烈的看法😈。这次甚至不会是“取决于”😅(几乎)。我今天要分享的是系统,我已经看到它在以下方面运行良好:
-
一个环境,在同一个存储库中有数十个松散连接的团队在同一产品上工作
-
在一个只有几个工程师的小型初创公司的快节奏环境中
-
甚至用于单人项目(是的,我一直将它用于我的个人项目)
请记住,与Pirate's Code相同,所有这些都更像是您所说的“指南”而不是实际规则。
项目结构约定我们需要什么
首先,我不想详细说明为什么我们需要这样的约定:如果您阅读了这篇文章,您可能已经决定需要它。不过,在进入解决方案之前,我想谈一点,是什么让项目结构约定变得很棒。
可复制性
代码约定应该易于理解并且易于被团队的任何成员复制,包括最近加入的具有最少 React 经验的实习生。如果在你的 repo 中工作的方式需要一个博士学位,几个月的培训和对每一秒 PR 进行深入的哲学辩论......嗯,它可能会是一个非常漂亮的系统,但它不会存在于纸上以外的任何地方。
可推断性
你可以写一本书,拍几部电影关于“我们回购的工作方式”。您甚至可以说服团队中的每个人阅读和观看它(尽管您可能不会)。事实仍然存在:大多数人不会记住它的每一个字,如果有的话。为了使约定真正起作用,它应该如此明显和直观,以便团队中的人理想地能够通过阅读代码对其进行逆向工程。在完美的世界里,就像代码注释一样,你甚至不需要在任何地方写下来——代码和结构本身就是你的文档。
独立
多人,尤其是多个团队的编码结构指南中最重要的要求之一是巩固开发人员独立操作的方式。您最不希望看到的是多个开发人员在同一个文件上工作,或者团队不断侵犯彼此的职责范围。
因此,我们的编码结构指南应该提供这样的结构,团队能够在同一个存储库中和平共存。
针对重构进行了优化
最后一个,但在现代前端世界中,它是最重要的一个。今天的前端非常流畅。模式、框架和最佳实践在不断变化。最重要的是,我们现在有望快速交付功能。不,快。然后一个月后完全重写。然后可能会重新编写一遍。
因此,对于我们的编码约定来说,不要强迫我们将代码“粘”在某个永久的地方而无法移动它就变得非常重要。它应该以这样一种方式组织事情,即重构是每天随意执行的事情。约定可以做的最糟糕的事情是使重构变得如此困难和耗时,以至于每个人都害怕它。相反,它应该像呼吸一样简单。
...
现在,我们对项目结构约定有了一般要求,是时候详细介绍了。让我们从大局开始,然后深入了解细节。
组织项目本身:分解
组织一个符合我们上面定义的原则的大型项目的第一个也是最重要的部分是“分解”:与其将其视为一个整体项目,不如将其视为或多或少独立特征的组合.旧的“单体”与“微服务”讨论,仅在一个 React 应用程序中。使用这种方法,每个特性在某种程度上本质上都是一个“纳米服务”,它与其他特性隔离并通过外部“API”(通常只是 React props)与它们通信。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--1vabJZKy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/cea5u7cx5acm4qkufpr0.png)
即使只是遵循这种心态,与更传统的“React 项目”方法相比,也会为您提供上面列表中的几乎所有内容:如果团队/人员将功能实现为一堆“黑盒子”相互连接。如果设置正确,那么对于任何人来说都应该很明显,只是需要一些练习来适应思维转变。如果您需要删除某个功能,您可以“拔掉”它,或者用另一个功能替换它。或者,如果您需要重构功能的内部结构,您可以这样做。只要它的公共“API”保持正常运行,外界就不会注意到它。
我在描述一个 React 组件,不是吗? 😅 嗯,概念是一样的,这使得 React 非常适合这种心态。我将定义一个“功能”,以将其与“组件”区分开来,“从最终用户的角度来看,一堆组件和其他元素捆绑在一起,形成一个完整的功能”。
现在,如何为单个项目组织这个?特别是考虑到,与微服务相比,它的管道要少得多:在一个具有数百个特性的项目中,将它们全部提取到实际的微服务中几乎是不可能的。相反,我们可以做的是使用多包 monorepo架构:它非常适合将独立功能组织和隔离为包。包是任何从 npm 安装任何东西的人都应该熟悉的概念。而一个 monorepo - 只是一个 repo,你有多个包的源代码和谐地生活在一起,共享工具、脚本、依赖关系,有时还相互共享。
所以概念很简单:React 项目 → 将其拆分为独立的功能 → 将这些功能放入包中。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--0vH7ht78--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/0rj12qg7z1a134gbgpbi.png)
如果你从来没有在本地设置过monorepo,现在,在我提到“package”和“npm”之后,对发布你的私人项目的想法感到不安:别这样。发布和开源都不是 monorepo 存在的必要条件,也不是开发人员从中受益的必要条件。从代码的角度来看,一个包只是一个文件夹,它有package.json个文件和一些属性。然后,该文件夹通过 Node 的符号链接链接到node_modules文件夹,其中安装了“传统”包。此链接由Yarn或Npm等工具本身执行:它被称为“工作区”,它们都支持它。并且它们使您可以在本地代码中访问包,就像从 npm 下载的任何其他包一样。
它看起来像这样:
/packages
/my-feature
/some-folders-in-feature
index.ts
package.json // this is what defines the my-feature package
/another-feature
/some-folders-in-feature
index.ts
package.json // this is what defines the another-feature package
进入全屏模式 退出全屏模式
在 package.json 中,我将拥有这两个重要字段:
{
"name": "@project/my-feature",
"main": "index.ts"
}
进入全屏模式 退出全屏模式
显然,“名称”字段是包的名称——基本上是这个文件夹的别名,通过它可以访问 repo 中的代码。而“main”是包的主要入口点,即当我写类似的东西时要导入哪个文件
import { Something } from '@project/my-feature';
进入全屏模式 退出全屏模式
有不少知名项目的公共存储库使用多包 monorepo 方法:Babel、React、Jest等等。
为什么是包而不仅仅是文件夹
乍一看,包的方法看起来像“只是将你的功能拆分到文件夹中,有什么大不了的”,并且似乎并不具有开创性。然而,包可以给我们一些有趣的东西,而简单的文件夹不能。
别名。使用包,您可以通过名称而不是位置来引用您的功能。比较一下:
import { Button } from '@project/button';
进入全屏模式 退出全屏模式
使用这种更“传统”的方法:
import { Button } from '../../components/button';
进入全屏模式 退出全屏模式
在第一次导入中,很明显 - 我正在使用我的项目的通用“按钮”组件,我的设计系统版本。
在第二个中,还不是很清楚——这个按钮是什么?它是通用的“设计系统”按钮吗?或者也许是这个功能的一部分?还是“高于”的功能?我什至可以在这里使用它,也许它是为一些非常具体的用例而编写的,这些用例在我的新功能中不起作用?
如果你的仓库中有多个“utils”或“common”文件夹,情况会变得更糟。我最糟糕的代码噩梦是这样的:
import { bla } from '../../../common';
import { blabla } from '../../common';
import { blablabla } from '../common';
进入全屏模式 退出全屏模式
使用包它可能看起来像这样:
import { bla } from '@project/button/common';
import { blabla } from '@project/something/common';
import { blablabla } from '@project/my-feature/common';
进入全屏模式 退出全屏模式
什么来自哪里,什么属于哪里,一目了然。很有可能,“我的功能”“通用”代码只是为了功能的内部使用而编写的,从来没有打算在功能之外使用,在其他地方重新使用它是一个坏主意。使用包,您会立即看到它。
关注点分离。考虑到我们都习惯了 npm 的包以及它们所代表的内容,当您立即将其编写为“包”时,将您的功能视为具有自己的公共 API 的独立模块会变得容易得多。
看看这个:
import { dateTimeConverter } from '../../../../button/something/common/date-time-converter';
进入全屏模式 退出全屏模式
与这个:
import { dateTimeConverter } from '@project/button';
进入全屏模式 退出全屏模式
第一个可能会在它周围的所有导入中丢失并且被忽略,从而将您的代码变成The Big Ball of Mud。第二个会立即自然地引起一些人的注意:日期时间转换器?从一个按钮?真的吗?这自然会在不同的功能/包之间形成更清晰的界限。
内置支持。您不需要发明任何东西,大多数现代工具,如 IDE、打字稿、linting 或打包程序都支持开箱即用的包。
重构轻而易举。将功能分离到包中,重构变得令人愉快。想要重构包的内容?继续,你可以完全重写它,只要你保持条目的 API 相同,repo 的其余部分甚至都不会注意到它。想要将您的包裹移动到另一个位置?如果你不重命名它只是一个文件夹的拖放,repo 的其余部分不受影响。想要重命名包?只需搜索和替换项目中的字符串,仅此而已。
明确的切入点。如果您想真正接受“仅适用于消费者的公共 API”的心态,您可以非常具体地了解外部消费者可以使用包中的哪些内容。例如,您可以限制所有“深度”导入,使诸如@project/button/some/deep/path之类的事情变得不可能,并强制每个人只使用 index.ts 文件中明确定义的公共 API。查看包入口点和包导出文档以了解其工作原理。
如何将代码拆分成包
人们在多包架构中遇到的最大问题是将代码提取到包中的正确时间是什么时候?每个小功能都应该是一个吗?或者,也许软件包仅适用于整个页面甚至是应用程序之类的大事?
根据我的经验,这里有一个平衡点。你不想把每一个小东西都提取到一个包中:你最终会得到一个简单的列表,其中包含数百个没有结构的只有一个文件的小包,这有点违背了最初引入它们的目的。同时,你不希望你的包变得太大:你会遇到我们在这里试图解决的所有问题,只在那个包中。
以下是我通常使用的一些边界:
- “设计系统”类型的东西,如按钮、模式对话框、布局、工具提示等,都应该是包
一些“自然”UI 边界中的* 个功能非常适合打包 - 即存在于模式对话框、抽屉、滑入式面板等中的东西
-
个“可共享”功能 - 可以在多个地方使用的功能
-
您可以将其描述为具有清晰边界、逻辑且理想地在 UI 中可见的孤立“功能”
另外,和上一篇文章如何将代码拆分成组件一样,一个包只负责一个概念上的事情是非常重要的。一个导出Button、CreateIssueDialog和DateTimeConverter的包一次做的事情太多,需要拆分。
如何整理包裹
虽然可以只创建所有包的平面列表,并且对于某些类型的项目它可以工作,但对于大型 UI 重的产品可能还不够。看到像“工具提示”和“设置页面”这样的包坐在一起让我感到畏缩。或者更糟——如果你同时拥有“后端”和“前端”包。这不仅混乱而且危险:您最不希望的是将一些“后端”代码意外拉入前端包中。
实际的 repo 结构在很大程度上取决于您正在实施的产品到底是什么(或者甚至有多少产品),您是否只有后端或前端,并且可能会随着时间的推移而发生重大变化和发展。幸运的是,这是包的巨大优势:实际结构完全独立于代码,如果需要,您可以每周一次拖放和重新构建它们,而不会产生任何后果。
考虑到结构中“错误”的成本相当低,至少在开始时不需要过度思考。如果你的项目是纯前端的,你甚至可以从一个平面列表开始:
/packages
/button
...
/footer
/settings
...
进入全屏模式 退出全屏模式
并随着时间的推移演变成这样的东西:
/packages
/core
/button
/modal
/tooltip
...
/product-one
/footer
/settings
...
/product-two
...
进入全屏模式 退出全屏模式
或者,如果你有一个后端,它可能是这样的:
/packages
/frontend
... // the same as above
/backend
... // some backend-specific packages
/common
... // some packages that are shared between frontend and backend
进入全屏模式 退出全屏模式
在“common”中,您将放置一些在前端和后端之间共享的代码。通常它将是一些配置、常量、类似 lodash 的实用程序、共享类型。
如何构造一个包本身
总结上面的大部分内容:“使用 monorepo,将特征提取到包中”。 🙂 现在到下一部分 - 如何组织包本身。对我来说,这里有三件事很重要:命名约定、将包分成不同的层和严格的层次结构。
命名约定
每个人都喜欢命名事物,并且喜欢争论别人在命名事物方面有多糟糕,不是吗?为了减少在无休止的 GitHub 评论线程上浪费的时间,并让像我这样与代码相关的强迫症患者平静下来,最好只为每个人就一个命名约定达成一致。
在我看来,使用哪一个并不重要,只要在整个项目中始终如一地遵循即可。如果你在同一个 repo 中有ReactFeatureHere.ts和react-feature-here.ts,小猫会在某处哭泣😿。我通常使用这个:
/my-feature-name
/assets // if I have some images, then they go into their own folder
logo.svg
index.tsx // main feature code
test.tsx // tests for the feature if needed
stories.tsx // stories for storybooks if I use them
styles.(tsx|scss) // I like to separate styles from component's logic
types.ts // if types are shared between different files within the feature
utils.ts // very simple utils that are used *only* in this feature
hooks.tsx // small hooks that I use *only* in this feature
进入全屏模式 退出全屏模式
如果一个特性有几个较小的组件直接导入index.tsx,它们看起来像这样:
/my-feature-name
... // the same as before
header.tsx
header.test.tsx
header.styles.tsx
... // etc
进入全屏模式 退出全屏模式
或者,更有可能的是,我会立即将它们提取到文件夹中,它们看起来像这样:
/my-feature-name
... // index the same as before
/header
index.tsx
... // etc, exactly the same naming here
/footer
index.tsx
... // etc, exactly the same naming here
进入全屏模式 退出全屏模式
文件夹方法对于复制粘贴驱动的开发更加优化😊:通过从附近的功能复制粘贴结构创建新功能时,您只需重命名一个文件夹即可。所有文件的名称将完全相同。此外,更容易创建包的心理模型、重构和移动代码(在下一节中介绍)。
包内的层
具有复杂功能的典型包将具有几个不同的“层”:至少“UI”层和“数据”层。虽然可能将所有内容混合在一起,但我仍然建议不要这样做:渲染按钮和从后端获取数据是截然不同的问题。将它们分开将使包更具结构性和可预测性。
为了使项目在架构和代码方面保持相对健康,关键是能够清楚地识别对您的应用程序很重要的那些层,映射它们之间的关系,并以某种方式组织所有这些这与使用的任何工具和框架保持一致。
如果我今天从头开始实施一个 React 项目,使用 Graphql 进行数据操作,使用纯 React 状态进行状态管理(即没有 Redux 或任何其他库),我将有以下几层:
-
**“数据”**层 - 查询、变异和其他负责连接到外部数据源并对其进行转换的事物。仅由 UI 层使用,不依赖于任何其他层。
-
“共享” 层 - 所有其他层在整个包中使用的各种实用程序、函数、挂钩、迷你组件、类型和常量。不依赖于任何其他层。
-
“ui” 层 - 实际的功能实现。依赖于“数据”和“共享”层,没有人依赖它
而已!
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--9IpJoURf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/99rwnd4qaub1npn98x7g.png)
如果我使用一些外部状态管理库,我可能还会添加“状态”层。那很可能是“数据”和“用户界面”之间的桥梁,因此将使用“共享”和“数据”层,而“用户界面”将使用“状态”而不是“数据”。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--ZzLZ2Y6c--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/5b5doucr662bgg698sqa.png)
而且从实现细节来看,所有层都是包中的顶级文件夹:
/my-feature-package
/shared
/ui
/data
index.ts
package.json
进入全屏模式 退出全屏模式
每个“层”都使用上述相同的命名约定。所以你的“数据”层看起来像这样:
/data
index.ts
get-some-data.ts
get-some-data.test.ts
update-some-data.ts
update-some-data.test.ts
进入全屏模式 退出全屏模式
对于更复杂的包,我可能会将这些层分开,同时保留它们的用途和特性。例如,“数据”层可以分为“查询”(“getters”)和“变异”(“setters”),它们可以仍然存在于“数据”文件夹中,也可以向上移动:
/my-feature-package
/shared
/ui
/queries
/mutations
index.ts
package.json
进入全屏模式 退出全屏模式
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--FEYMKaCC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/rj22ti2rppx9a6p6i4sm.png)
或者您可以从“共享”层中提取一些子层,例如“类型”和“共享 UI 组件”(顺便说一句,这会立即将此子层转换为“UI”类型,因为除了“UI”之外没有其他的可以使用 UI 组件)。
/my-feature-package
/shared-ui
/ui
/queries
/mutations
/types
index.ts
package.json
进入全屏模式 退出全屏模式
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--8gVzT8n9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/h1g30kli1atfj2k0wer9.png)
只要你能清楚地定义每个“子层”的目的是什么,清楚哪个“子层”属于哪个“层”,并且可以将其可视化并解释给团队中的每个人 - 一切正常!
层内严格的层次结构
使该架构可预测和可维护的难题的最后一部分是层内的严格层次结构。这在 UI 层中尤其明显,因为在 React 应用程序中它通常是最复杂的。
例如,让我们开始搭建一个带有页眉和页脚的简单页面。我们有“index.ts”文件——主文件,页面放在一起,还有“header.ts”和“footer.ts”组件。
/my-page
index.ts
header.ts
footer.ts
进入全屏模式 退出全屏模式
现在,它们都有自己的组件,我想将它们放入自己的文件中。例如,“标题”将具有“搜索栏”和“发送反馈”组件。在组织应用程序的“传统”扁平方式中,我们会将它们彼此相邻放置,不是吗?会是这样的:
/my-page
index.ts
header.ts
footer.ts
search-bar.ts
send-feedback.ts
进入全屏模式 退出全屏模式
然后,如果我想在页脚组件中添加相同的“send-feedback”按钮,我会再次将它从“send-feedback.ts”导入到“footer.ts”,对吧?毕竟,它就在附近,看起来很自然。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--ci4-M31r--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev- to-uploads.s3.amazonaws.com/uploads/articles/bj91bsthvurm9o6gz9mn.png)
不幸的是,刚刚发生的事情是我们违反了层之间的界限(“UI”和“共享”),甚至没有注意到它。如果我继续在这个扁平结构中添加越来越多的组件,而且我可能会这样做,那么实际应用程序往往会非常复杂,我可能会多次违反它们。这会将这个文件夹变成它自己的小“Ball Of Mud”,其中哪个组件依赖于哪个组件是完全不可预测的。结果,当重构时间到来时,解开所有这些并从这个文件夹中提取一些东西,可能会变成一个非常令人头疼的练习。
相反,我们可以以分层的方式构建这一层。规则是:
-
只有文件夹中的主文件(即“index.ts”)可以有子组件(子模块)并可以导入它们
-
只能从“孩子”导入,不能从“邻居”导入
-
你不能跳过一个级别,只能从直接孩子那里导入
或者,如果您更喜欢视觉,它只是一棵树:
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--Ml8WFc91--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/d5xafhug2ypqwtgnyjfs.png)
如果你需要在这个层次结构的不同级别之间共享一些代码(比如我们的发送反馈组件),你会立即发现你违反了层次结构的规则,因为无论你把它放在哪里,你都必须导入它来自父母或邻居。因此,它将被提取到“共享”层并从那里导入。
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--5vOQqUju--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/s26ivfkc6sw38abjq2u2.png)
看起来像这样:
/my-page
/shared
send-feedback.ts
/ui
index.ts
/header
index.ts
search-bar.ts
/footer
index.ts
进入全屏模式 退出全屏模式
这样,UI 层(或适用该规则的任何层)就变成了树结构,其中每个分支都独立于任何其他分支。从此包中提取任何内容现在变得轻而易举:您只需将文件夹拖放到新位置即可。你肯定知道,除了实际使用它的那个之外,UI 树中没有一个组件会受到它的影响。您可能需要额外处理的唯一一件事是“共享”层。
带有数据层的完整应用程序将如下所示:
[
](https://res.cloudinary.com/practicaldev/image/fetch/s--jOhZgjPO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to- uploads.s3.amazonaws.com/uploads/articles/dhbomxrfhjzwm6abhl4n.png)
一些明确定义的层,它们是完全封装和可预测的。
/my-page
/shared
send-feedback.ts
/data
get-something.ts
send-something.ts
/ui
index.ts
/header
index.ts
search-bar.ts
/footer
index.ts
进入全屏模式 退出全屏模式
React 建议不要嵌套
如果您阅读有关推荐项目结构的 React 文档,您会看到 React 实际上推荐反对过多的嵌套。官方建议是“考虑将自己限制在单个项目中最多三个或四个嵌套文件夹”。这个建议也与这种方法非常相关:如果你的包变得过于嵌套,这是一个明显的迹象,你可能需要考虑将它拆分成更小的包。以我的经验,3-4 级的嵌套对于非常复杂的功能来说也足够了。
包架构的美妙之处在于,您可以根据需要使用尽可能多的嵌套来组织包,而不受此限制的约束——您永远不会通过其相对路径引用另一个包,而只能通过其名称。位于路径packages/change-settings-dialog或隐藏在/packages/product/features/settings-page/change-setting-dialog内的名为@project/change-setting-dialog的包将被称为@project/change-setting-dialog,无论其物理位置如何。
Monorepo 管理工具
如果不接触至少一点 monorepo 管理工具,就不可能为你的架构谈论多包 monorepo。最大的问题通常是其中的依赖管理。想象一下,如果您的某些 monorepo 包使用外部依赖项,例如lodash。
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
进入全屏模式 退出全屏模式
现在 lodash 发布了一个新版本,lodash@4.0.0,你想把你的项目移到它上面。您需要同时在所有地方更新它:您最不想要的是保留在旧版本上的一些软件包,而一些使用新版本。如果您使用的是npm或旧的yarn,那将是一场灾难:他们会在您的系统中安装lodash的多个副本(而不是两个,多个),这将导致安装和构建时间增加,以及您的捆绑包大小穿过屋顶。更不用说在整个项目中使用同一个库的两个不同版本时开发新功能的乐趣。
如果您的项目要在npm上发布并开源,我不会涉及使用什么:可能像Lerna之类的就足够了,但这是一个完全不同的话题。
但是,如果您的仓库是私有的,事情就会变得更加有趣。因为要使该架构正常工作,您真正需要的只是包“别名”,仅此而已。 IE。只是基本的符号链接,Yarn和Npm通过工作空间的概念提供。它看起来像这样。您有“根”package.json文件,您可以在其中声明工作空间(即您的本地包)的位置:
{
"private": true,
"workspaces": ["packages/**"]
}
进入全屏模式 退出全屏模式
然后下次运行yarn install时,文件夹 packages 中的所有包都将变成“正确的”包,并通过它们的名称在您的项目中可用。这就是整个 monorepo 设置!
至于依赖。如果您在几个包中具有相同的依赖关系,会发生什么?
/packages
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
进入全屏模式 退出全屏模式
当你运行yarn install时,它会将该包“提升”到根node_modules:
/node_modules
lodash@3.4.5
/packages
/my-feature-one
package.json // this one uses lodash@3.4.5
/my-other-feature
package.json // this one uses lodash@3.4.5
进入全屏模式 退出全屏模式
这与您仅在根package.json中声明lodash@3.4.5完全一样。我要说的是,我可能会为此被互联网的纯粹主义者活埋,包括两年前的我自己:你不需要在本地包中声明任何依赖项。一切都可以转到根package.json。本地包中的package.json文件将只是非常轻量级的json文件,仅指定“名称”和“主”字段。
设置更容易管理,特别是如果您刚刚开始。
React 项目结构的规模:最终概述
嗯,那是很多文字。甚至这只是一个简短的概述:关于这个话题还有很多事情可以说!让我们至少回顾一下已经说过的话:
分解是成功扩展 React 应用的关键。不要将您的项目视为一个单一的“项目”,而是将独立的黑盒(如“功能”)与他们自己的公共 API 相结合,供消费者使用。确实与“单体”与“微服务”的讨论相同。
Monorepo 架构 非常适合。将您的功能提取到包中;以最适合您的项目的方式组织您的包。
一个包中的层对于赋予它一些结构很重要。你可能至少会有“数据”层、“UI”层和“共享”层。可以引入更多,根据你的需要,只需要它们之间有明确的界限。
包的层次结构很酷。它使重构更容易,迫使您在层之间有更清晰的界限,并迫使您在包变得太大时将其拆分为较小的包。
monorepo 中的依赖管理 是一个复杂的话题,但如果你的项目是私有的,你实际上不需要担心它。只需在根 package.json 中声明所有依赖项,并使所有本地包不受它们影响。
您可以在此示例 repo 中查看此架构的实现:https://github.com/developerway/example-react-project。这只是演示文章中描述的原理的一个基本示例,所以不要被只有一个 index.ts 的小包吓到:在真正的应用程序中它们会更大。
这就是今天的全部内容。希望您能够将其中一些原则(甚至全部!)应用到您的应用程序中,并立即看到日常开发的改进! ✌🏼
...
最初发表于https://www.developerway.com。该网站有更多这样的文章😉
订阅时事通讯、在 LinkedIn 上连接或在 Twitter 上关注以在下一篇文章发布后立即获得通知。
更多推荐

所有评论(0)