愉快的React文件/目录结构

165 阅读15分钟

简介

当涉及到文件/目录结构时,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 。我曾经在一些项目中工作过,那里的顶栏是这样的。

A bunch of files open, all called “index.js”

公平地说,现在大多数编辑器在同时打开多个index.js 文件时都会包括父目录,但这样一来,每个标签就会占用更多的空间。

A bunch of files open, formatted like “index.js - /RainbowButton”

A bunch of files open, formatted like “index.js - /RainbowButton”

我的目标是要有漂亮、干净的组件文件名,像这样。

A bunch of files open, with proper names like “RainbowButton.js”

A bunch of files open, with proper names like “RainbowButton.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.jsindex.js 做了一个隐喻性的301 REDIRECTsrc/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政策

好吧,所以你会注意到我在这里选择做两件事。

  1. 我使用kebab-case而不是camelCase。

  2. 我在每个文件名的末尾添加.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日

点击率