简介
当涉及到文件/目录结构时,React是出了名的没有主见的。你应该如何构建你的应用程序中的文件和目录?
好吧,没有一个 "正确 "的方法,但是在我使用React的7年多时间里,我尝试了很多不同的方法,并且我迭代了我的方法,找到了我真正满意的解决方案。
在这篇博文中,我将分享我在当前所有项目中使用的结构,包括这个博客和我的定制课程平台。
[
链接到这个标题
](www.joshwcomeau.com/#interactiv…
好吧,我会深入解释一切,但我想让你先来个自助游会很有趣。
这里有一个交互式文件资源管理器。请随意浏览,看看东西是如何结构的
控件
组件
应用程序
App.js
index.js
标题
Header.js
index.js
小工具
Widget.constants.js
Widget.helpers.js
小工具.js
WidgetChild.js
index.js
us-stuff.hook.js
帮助器
animation.helpers.js
auth.helpers.js
钩子
使用boop.hook.js
us-bounding-box.hook.js
constants.js
脚本.js
package.json
README.md
import * as CONSTANTS from './Widget.constants';
这个演示中的文件是JavaScript,但这种结构对TypeScript项目也同样适用
[
链接到这个标题
](www.joshwcomeau.com/#my-priorit…
让我们先来谈谈我的优先事项,也就是我所优化的东西。
首先,我想让导入组件变得简单。我希望能够写出这个。
js
下一篇:当我在我的IDE中工作时,我不希望被淹没在index.js
。我曾经在一些项目中工作过,那里的顶栏是这样的。
公平地说,现在大多数编辑器在同时打开多个index.js
文件时都会包括父目录,但这样一来,每个标签就会占用更多的空间。
我的目标是要有漂亮、干净的组件文件名,像这样。
最后,在组织方面,我希望东西是按功能组织的,而不是按特性。我想要一个 "组件 "目录,一个 "钩子 "目录,一个 "助手 "目录,等等。
有时,一个复杂的组件会有一堆相关的文件。这些文件包括。
-
"子组件",由主组件专门使用的较小的组件
-
辅助功能
-
自定义钩子
-
组件和其相关文件之间共享的常量或数据
作为一个真实的例子,让我们来谈谈FileViewer
,在这篇博文中用于 "交互式文件浏览器 "的演示。下面是专门为这个组件创建的文件。
-
FileViewer.js
- 主组件 -
FileContent.js
- 显示文件内容的组件,带有语法高亮功能 -
Sidebar.js
- 可以探索的文件和目录的列表 -
Directory.js
- 可折叠的目录,在侧边栏中使用 -
File.js
- 在侧边栏中使用的单个文件 -
FileViewer.helpers.js
- 帮助函数,用于遍历树,并帮助管理扩展/折叠功能
理想情况下,所有这些文件都应该被藏起来,不被人看到。只有当我在做FileViewer
组件时才需要它们,所以我应该只在做FileViewer
时看到它们。
一个组件一个文件?
有一段时间,一个流行的ESLint规则警告说不要在每个文件中定义超过一个组件。
我认为这是个愚蠢的规则。文件可以包含你想要的任何数量的组件。
也就是说,我发现自己一旦有了基本的功能,通常会把非重要的组件拉到自己的文件中。这有助于保持事情的条理性,并使哪些组件使用了哪些导入/样式/什么的变得非常清楚。
[
链接到这个标题
](www.joshwcomeau.com/#the-implem…
好了,让我们来谈谈我的实现如何解决这些优先事项。
[
链接到这个标题
](www.joshwcomeau.com/#components…
这里有一个组件的例子,其中包含了为实现我的目标所需的所有文件和目录。
这些文件中的大部分都是前面提到的,即FileViewer
组件所需要的文件。唯一的例外是index.js
。这是新的。
如果我们打开它,我们会看到一个有点奇怪的东西。
js
这本质上是一个重定向。当我们试图导入这个文件时,我们会被转发到同一目录下的FileViewer.js
文件。FileViewer.js
包含这个组件的实际代码。
**为什么不把代码直接保留在index.js
?**那么,我们的编辑器就会被index.js
文件填满!我不想这样。我不希望这样。
**为什么要有这个文件呢?**它简化了导入。否则,我们将不得不钻进组件目录,手动选择文件,像这样。
js
有了我们的index.js
转发,我们可以把它缩短为只是。
js
**为什么这样做呢?**嗯,FileViewer
是一个目录,当我们试图导入一个目录时,捆绑器会寻找一个索引文件(index.js
,index.ts
, 等等)。这是一个从网络服务器延续下来的惯例:my-website.com
会自动尝试加载index.html
,这样用户就不必写my-website.com/index.html
。
事实上,我认为从HTTP请求的角度来考虑这个问题是有帮助的。当我们导入src/components/FileViewer
,捆绑器将看到我们正在导入一个目录,并自动加载index.js
。index.js
做了一个隐喻性的301 REDIRECT到src/components/FileViewer/FileViewer.js
。
它可能看起来过于工程化,但这种结构满足了我所有的要求,我喜欢它。
[
链接到这个标题
](www.joshwcomeau.com/#hooks)钩子
如果一个钩子是特定于某个组件的,我就把它和那个组件放在一起。但如果这个钩子是通用的,而且是要被很多组件使用的呢?
在这个博客中,我有大约50个通用的、可重用的钩子。它们被收集在src/hooks
目录中。这里有一些例子。
src
钩子
use-boop.hook.js
us-bounding-box.hook.js
使用e-effect-on-change.hook.js
use-has-mounted.hook.js
使用is-onscreen.hook.js
使用throttled-state.hook.js
使用窗口尺寸.钩子.js
使用worker.hook.js
README.md
// For more information on this hook:
(这段代码是真实的!这里提供的是一个例子,但请随意复制你感兴趣的钩子。)
命名规则YOLO政策
好吧,所以你会注意到我在这里选择做两件事。
-
我使用kebab-case而不是camelCase。
-
我在每个文件名的末尾添加
.hook
。
我说实话:我做这些决定并没有什么好的理由。我只是喜欢它看起来的样子。😄
你可能更喜欢将你的钩子命名为useThing.js
,而不是use-thing.hook.js
,这完全没有问题。你使用哪种约定的文件名其实并不重要。
[
链接到这个标题
](www.joshwcomeau.com/#helpers)帮助…
如果我有一个函数可以帮助我完成项目的某些目标,而不是直接与某个特定的组件相联系呢?
例如:这个博客有多个博文类别,如React、CSS和Animations。我有一些函数可以帮助我按照文章的数量对分类进行排序,或者为它们获得格式化/"漂亮 "的名字。所有这些东西都住在一个category.helpers.js
文件中,在src/helpers
。
有时,一个函数会在一个特定组件的文件中开始(例如:FileViewer/FileViewer.helpers.js
),但我意识到我需要它在多个地方。它就会被移到src/helpers
。
[
链接到这个标题
](www.joshwcomeau.com/#utils)实用程序
好吧,这个问题需要解释一下。
很多开发者把 "helpers "和 "utils "当作同义词,但我对它们进行了区分。
帮助器是特定项目的东西。一般来说,在项目之间共享帮助器是没有意义的;category.helpers.js
,这些功能只对这个博客有意义。
实用程序是一个通用函数,用于完成一个抽象的任务。根据我的定义,lodash
库中的每个函数几乎都是一个实用程序。
例如,这里有一个我经常使用的实用程序。它从一个数组中随机抽出一个项目。
js
我有一个utils.js
文件,里面全是这类实用程序的函数。
**为什么不使用一个成熟的实用程序库,比如lodash?**有时我会这样做,如果它不是我自己可以轻易建立的东西。但没有一个实用程序库会有我需要的所有实用程序。
例如,这个工具可以在一个文本输入中移动用户的光标。
js
这个实用程序可以获得笛卡尔平面上两点之间的距离(这在有非简单动画的项目中经常出现,令人惊讶)。
js
这些实用程序存在于src/utils.js
,它们随着我的项目而来。当我创建一个新项目时,我会复制/粘贴该文件。我可以通过NPM发布它,以确保项目之间的一致性,但这将增加大量的摩擦,对我来说,这不是一个值得的权衡。也许在某个时候,但还没有。
[
链接到这个标题
](www.joshwcomeau.com/#constants)…
最后,我也有一个constants.js
文件。这个文件保存了整个应用程序的常量。其中大部分与风格有关(例如,颜色、字体大小、断点),但我也在这里存储公钥和其他 "应用程序数据"。
[
链接到这个标题
](www.joshwcomeau.com/#pages)页面
这里没有显示的一件事是 "页面 "的概念。
我省略了这一部分,因为这取决于你使用什么工具。当我使用create-react-app这样的工具时,我没有页面,所有的东西都是组件。但当我使用Next.js时,我确实有/src/pages
,有顶层组件来定义每个路由的大致结构。
[
链接到这个标题
](www.joshwcomeau.com/#tradeoffs)…
每种策略都有取舍。让我们来谈谈本博文中概述的文件结构方法的一些弊端。
[
链接到这个标题
](www.joshwcomeau.com/#more-boile…
每当我想创建一个新的组件时,我需要生成。
-
一个新的目录。
Widget/
-
一个新的文件。
Widget/Widget.js
-
索引转发器。
Widget/index.js
这是很重要的前期工作!
幸运的是,**我不需要手动做这些事。**我创建了一个NPM包,new-component,它为我自动完成所有这些工作。
在我的终端,我输入
当我执行这个命令时,所有的模板都会为我创建,包括我必须要输入的基本组件结构!这真是节省了大量的时间。这是一个令人难以置信的节省时间的方法,在我看来,它完全抵消了这个缺点。
如果你愿意的话,欢迎你使用我的包。你可能想把它分叉,以符合你的首选惯例。
[
链接到这个标题
](www.joshwcomeau.com/#organized-…
一般来说,有两种广泛的方式来组织事情。
-
按功能(组件、钩子、助手...)。
-
按功能(搜索、用户、管理...)。
这里有一个例子,说明如何按功能来组织代码。
我非常喜欢这样做。它使低级别的可重复使用的 "组件库 "类型的组件与高级别的模板式视图和页面分开成为可能。而且它使我们更容易快速了解应用程序的结构。
**但问题是:**现实生活并不像这样被很好地分割开来,而且分类实际上是非常困难的。
我曾经参与过几个采用这种结构的项目,每次都有几个重要的摩擦源。
每当你创建一个组件时,你就必须决定这个组件属于哪里。如果我们创建一个组件来搜索一个特定的用户,那是属于 "搜索 "的范畴,还是属于 "用户 "的范畴?
通常情况下,界限是模糊的,不同的开发者会围绕什么应该放在哪里做出不同的决定。
当我开始一个新功能的工作时,我必须找到这些文件,而它们可能不在我期望的地方。项目中的每个开发人员都会有自己的概念模型,知道什么应该放在哪里,我需要花时间适应他们的观点。
然后,还有一个真正的大问题: 重构。
产品总是在不断发展和变化,我们今天在功能周围划定的界限明天可能就没有意义了。当产品发生变化时,将需要大量的工作来移动和重命名所有的文件,对所有的东西进行重新分类,使其与下一版本的产品相协调。
**现实上,这些工作实际上是不会完成的。**这太麻烦了;团队已经在工作了,他们有一堆半成品的PR,他们都在编辑文件,如果我们把所有的文件都搬来搬去,这些文件就不复存在了。管理这些冲突是可能的,但这是一个很大的痛苦。
因此,产品特性和代码特性之间的距离会越拉越远。最终,代码库中的功能将在概念上围绕一个不再存在的产品组织起来,因此每个人都必须记住所有东西的位置。边界不再是直观的,而是完全任意的,最好的情况是误导。
公平地说,有可能避免这种最坏的情况,但在我看来,这是一个大量的额外工作,但好处相对较少。
**但是,另一种情况不是太混乱了吗?**大型项目有成千上万的React组件是很常见的。如果你遵循我的基于功能的方法,这意味着你会有一大批无组织的组件并排放在src/components
。
这听起来可能是个大问题,但说实话,我觉得这是个小代价。至少你知道该去哪里找!你不必在几十个功能中寻找你要找的文件。而且,你需要花0秒时间来弄清楚你创建的每个新文件的位置。
[
链接到这个标题
](www.joshwcomeau.com/#webpack-al…
Webpack是用于在部署前将我们的代码打包的捆绑器。还有其他的捆绑器,但大多数常见的工具(如come-react-app、Next.js、Gatsby)会在内部使用Webpack。
一个流行的Webpack功能让我们创建别名,即指向特定文件或目录的全局名称。比如说。
js
**下面是它的工作原理。**我创建了一个叫@helpers
的别名,它将指向/src/helpers
目录。每当捆绑器看到@helpers
,它就会用该目录的相对路径替换这个字符串。
主要的好处是,它把一个相对路径(../../helpers
)变成了一个绝对路径(@helpers
)。我再也不用考虑需要多少层的../
了。而且,当我移动文件时,我不需要修复/更新任何导入路径。
实现Webpack别名超出了这篇博文的范围,而且会根据使用的元框架而有所不同,但你可以在Webpack文档中了解更多。
别名的权衡
像所有美好的事物一样,Webpack别名也是有取舍的。
最大的问题是,Webpack的别名是非标准的。你正在远离原生的JavaScript导入,做一些自定义的事情。这就把你锁住了。
它也会使你的代码更难与第三方服务一起使用。如果你想用Storybook建立一个组件库,或者用Jest创建单元测试,这些工具将需要被配置成能够理解你的Webpack别名。在大多数情况下,这是有可能的,但要弄清楚这一点可能很棘手。绝对是一个额外的摩擦源。
此外,一些编辑器会在自动完成方面遇到困难,尽管这通常也可以配置。对于VS Code来说,我们可以在jsconfig.json
。 下面是我的文件,作为一个例子。
显示更多
[
链接到这个标题
](www.joshwcomeau.com/#something-…
所以,这就是我如何构建我的React应用程序的方法
正如我在顶部提到的,管理文件结构没有正确/错误的方法。每种方法都会优先考虑不同的事情,做出不同的权衡。
不过,就我个人而言,我发现这种结构不影响我的工作。我可以把时间花在我喜欢的事情上:建立高质量的用户界面。
React是如此的有趣。我从2015年开始使用它,当我能用React工作时,我仍然感到兴奋。
有几年时间,我在当地的一个编码训练营教书。我与大量的开发者一对一地工作,回答他们的问题,帮助他们摆脱困境。我最终为这所学校的所有教员开发了课程。
我想和更多的人分享React的乐趣,所以我正在做一些新的工作。一个在线课程,教你如何用React构建复杂、丰富、异想天开、无障碍的应用程序。
现在分享还为时过早,但如果你想跟上,最好的方法是通过我的新闻通讯。你将会是第一个听到课程更新的人,以及我发布的任何新的博客文章的人。
姓氏
电子邮件
你同意这些条款吗?
如果你是一个人,请忽略这个字段。
订阅
平均而言,我每月发送约1期。没有垃圾邮件,没有胡言乱语。如果你不喜欢它,你可以一键取消订阅。请看最近的一期!
[
链接到这个标题
](www.joshwcomeau.com/#bonus-expl…
你是否好奇我是如何建立上面那个FileViewer
组件的?
老实说,这不是我最好的作品。但我确实遇到了一些有趣的挑战,试图用React渲染一个递归结构
如果你好奇它是如何工作的,你可以使用FileViewer组件来探索FileViewer的源代码。虽然没有提供所有的上下文,但它应该能让你对它的工作原理有一个相当好的了解
src
组件
文件查看器
Directory.js
Expander.js
File.js
FileContent.js
FileViewer.helpers.js
FileViewer.js
index.js
Sidebar.js
import React from 'react';
最后更新
2022年3月15日