优雅的React 文件/目录 结构(Vue适用)

1,820 阅读10分钟

前言

建个文件、目录简单,但是你有没有考虑过,如何优雅的构建前端项目的文件和目录?

建立的文件和目录显得专业化,让人一看就称呼你为大牛。

在过去开发的境遇里,我尝试了很多不同的方法,并且我已经迭代了我非常满意的解决方案。

在这篇文章中,我将分享我在当前所有项目中使用的结构,文章以 React 项目为例子介绍, 同样你也可以在 Vue 项目中使用,两者之间并不会有太大的差距, 同样你也不必因为是React 而觉得毫无收获

文件目录展示

image.png

需求分析

第一:我想使导入组件变得容易。

我希望能够写这个:

import Button from '../Button';
// 使用Webpack别名
import Button from '@components/Button';

第二:拥有漂亮、干净的组件文件名

当我在 IDE 中工作时,我不想顶部打开的文件栏被淹没。例如顶部栏如下所示的项目中都是打开的:index.js

打开一堆文件,全部称为“index.js”

公平地说,当同时打开多个文件时,大多数编辑器现在都会包含父目录,但每个选项卡会占用更多空间:index.js

打开一堆文件,格式为“index.js - /RainbowButton”

我的目标是拥有漂亮、干净的组件文件名,如下所示:

打开一堆文件,具有专有名称,例如“RainbowButton.js”

第三 文件按功能来划分在一起而不是特性

我想要一个components目录,一个hooks目录,一个helpers目录,等等。

有时,一个复杂的组件会有一堆关联的文件。其中包括:

  • “子组件”,主要组件专用的较小组件
  • 帮助函数 function
  • 自定义钩子 hooks
  • 组件及其关联文件之间共享的常量或数据 data

作为一个真实的例子,以下是专门为此组件创建的文件:FileViewer

  • FileViewer.js— 主要组件
  • FileContent.js— 呈现文件内容的组件,具有语法突出显示
  • Sidebar.js— 可以浏览的文件和目录列表
  • Directory.js— 可折叠目录,在侧边栏中使用
  • File.js— 要在侧边栏中使用的单个文件
  • FileViewer.helpers.js— 帮助程序函数遍历树并帮助管理扩展/折叠功能

理想情况下,所有这些文件都应该隐藏起来,看不见。它们仅在我处理组件时需要,因此我应该只在处理 FileViewer

每个文件都需要一个组件?

我认为可以的。文件可以包含任意数量的组件!

一旦我有了基本功能,我通常会将之前的组件拉入自己现有项目文件中。 保持井井有条,并非常清楚地了解哪些组件使用了哪些导入/样式/任何内容。

开始实现

好的,让我们谈谈我的实现如何解决这些需求。

组件

下面是一个示例组件,其中包含实现目标所需的所有文件和目录:

src/
└── components/
    └── FileViewer/
        ├── Directory.js
        ├── File.js
        ├── FileContent.js
        ├── FileViewer.helpers.js
        ├── FileViewer.js
        ├── index.js // 新增的
        └── Sidebar.js

这些文件中的大多数都是前面提到的,即组件所需的文件。例外情况是多了一个index.js,这里面的代码仅有如下

export * from './FileViewer';
export { default } from './FileViewer';

这本质上是一种重定向。当我们尝试导入此文件时,将被转发到同一目录中的FileViewer.js文件。

为什么不直接保留代码在index.js

那么我们的编辑器将填满index.js文件!我不想这样。

为什么有这个文件? 它简化了导入。否则,我们必须钻取到组件目录并手动选择文件,如下所示:

import FileViewer from '../FileViewer/FileViewer';

通过我们的转发,我们可以将其缩短为:index.js

JS
import FileViewer from '../FileViewer';

为什么会这样?

FileViewer是一个目录,当我们尝试导入一个目录时,捆绑器会寻找一个索引文件(index.jsindex.ts等)。这是从Web服务器继承下来的约定:my-website.com将自动尝试加载index.html,这样用户就不必编写my-website.com/index.html

事实上,我认为从HTTP请求的角度来考虑这一点会有所帮助。当我们导入src/components/FileViewer时,捆绑器将看到我们正在导入一个目录并自动加载index.js。做一个隐喻性的301重定向src/components/FileViewer/FileViewer.js

它可能看起来设计过度,但这种结构满足了我所有的要求。

Hooks(钩子)

如果hooks特定于某个组件,我会将其与该组件放在一起。但是,如果钩子是通用的,并且打算由许多组件使用,该怎么办?

将他们它们收集在目录中src/hooks。以下是一些示例:

命名约定, YOLO 策略

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

  1. 我用'-'连字符代替小驼峰的写法。
  2. 我每个文件名的末尾添加.hook

老实说:我没有充分的理由做出这些决定。我只是喜欢它的样子。😄

您可能更喜欢useThing.js命名您的钩子而不是use-thing.hook.js,这完全没问题!对文件名使用哪种约定并不重要。

帮助 Helpers

如果我有一个函数可以帮助我完成项目的某个目标,而不是直接绑定到特定组件,该怎么办?

例如:一些排序的功能,可以帮助我将不同类型的产品按产品数量进行排序。所有这些东西都存在于一个文件中category.helpers.js,在src/helpers里面.

有时,一个函数会在特定于组件的文件中使用(例如FileViewer/FileViewer.helpers.js),但我会意识到我在多个地方都需要它。它将被移动到src/helpers .

实用工具

好的,所以这个需要一些解释。

许多开发人员将helpers 帮助utils 工具混为一谈,但我对它们进行了区分。

  • helpers 帮助是特定于给指定项目使用。在项目之间共享helpers 帮助通常没有意义;category.helpers.js内功能实际上只对这一个工程有意义。

  • utils 工具是完成抽象任务的通用函数。根据我的定义,lodash库中几乎每个函数都是一个实用程序。

例如,这是我经常使用的utils 工具。它从数组中随机提取一个项目:

export const sampleOne = (arr) => {
  return arr[Math.floor(Math.random() * arr.length)];
};

我有一个utils.js文件,充满这些实用程序功能。

为什么不使用像 lodash 这样的已建立的 utils 库?

有时我会这样做,但是有时候个性化需求就满足不了了,

例如,这个在文本输入中移动用户的光标:

export function moveCursorWithinInput(input, position) {
  if (input.setSelectionRange) {
    input.focus();
    input.setSelectionRange(position, position);
  } else if (input.createTextRange) {
    var range = input.createTextRange();
    range.collapse(true);
    range.moveEnd('character', position);
    range.moveStart('character', position);
    range.select();
  }
}

这个utils可以获取笛卡尔平面上两点之间的距离(在具有动画的项目中,这种情况经常令人惊讶地出现):

export const getDistanceBetweenPoints = (p1, p2) => {
  const deltaX = Math.abs(p2.x - p1.x);
  const deltaY = Math.abs(p2.y - p1.y);
  return Math.sqrt(deltaX ** 2 + deltaY ** 2);
};

这些 utils 存在于src/utils.js中,它们伴随着我从一个项目到另一个项目。我在创建新项目时复制/粘贴文件。我可以通过 NPM 发布它以确保项目之间的一致性,但这会增加大量的冲突,而且对我来说不值得的权衡。

常量

最后,我还有一个文件constants.js。此文件包含项目所需的常量。它们中的大多数都与样式相关(例如颜色、字体大小、断点),但我也在这里存储公钥和其他“常量数据”。

页面

这里没有显示的一件事是“页面”的概念。

我省略了这一部分,因为它取决于您使用的工具。当我使用像create-react-app这样的东西时,我没有页面,一切都是组件。但是当我使用 Next.js 时,我确实有 /src/pages,其中包含定义每个路由的粗略结构的顶级组件。

权衡

每种策略都有权衡取舍。让我们谈谈这篇博文中概述的文件结构方法的一些缺点。

按功能组织

通常,有两种广泛的方法来组织事物:

  • 按功能function(组件、钩子、助手等)
  • 按特征feature(搜索、用户、管理员等)

下面是如何按特性构建代码的示例:

src/
├── base/
│   └── components/
│       ├── Button.js
│       ├── Dropdown.js
│       ├── Heading.js
│       └── Input.js
├── search/
│   ├── components/
│   │   ├── SearchInput.js
│   │   └── SearchResults.js
│   └── search.helpers.js
└── users/
    ├── components/
    │   ├── AuthPage.js
    │   ├── ForgotPasswordForm.js
    │   └── LoginForm.js
    └── use-user.hook.js

有些时候我真的很喜欢这个。它根据视图模板进行区分,每个组件库只为当前的视图服务,你可以很好的定位到代码在哪,快速修改BUG。它使您可以更轻松地快速了解应用程序的结构。

但问题是

现实项目并没有像这样很好地分割,分类实际上真的很难

  • 我曾参与过一些采用这种结构的项目,每次都有几个来源(变量函数等)会有歧义。

  • 每次创建组件时,都必须确定该组件所属的位置。如果我们创建一个组件来搜索特定用户,这是“搜索”关注的一部分,还是“用户”关注的一部分?

所以: 通常,界限是模糊的,不同的开发人员会围绕什么应该去哪里做出不同的决定。

当我开始开发新功能时,我必须找到文件,它们可能不是我期望的位置。项目的每个开发人员都有自己的概念模型,用于确定应该去哪里,我需要花时间适应他们的观点。

然后还有一个真正的大问题: 重构

产品总是在不断发展和变化,我们今天围绕功能划定的界限明天可能就没有意义了。当产品发生变化时,移动和重命名所有文件需要大量的工作,对所有内容进行重新分类,以便与产品的下一个版本保持一致。

实际上 ,这项工作实际上不会完成。 太麻烦了;团队已经在做一些事情了,他们有一堆半成品的 PR,他们都在编辑文件,如果我们移动所有文件,这些文件将不再存在。管理这些冲突是可能的,但这是一个很大的痛苦。

因此,产品功能和代码功能之间的距离将越来越远。最终,代码库中的功能将在概念上围绕不再存在的产品进行组织,因此每个人都只需要记住所有内容的位置。边界不是直观的,而是充其量变得完全任意,最坏的情况是误导。

公平地说,可以避免**这种最坏的情况,但在我看来,这是很多额外的工作,而收益相对较少。

但替代方案是不是太混乱了? 大型项目拥有数千个 React 组件的情况并不少见。如果您遵循我基于函数的方法,这意味着您将拥有大量并排位于 src/components 中的无组织组件集。

这听起来可能很重要,但老实说,我觉得这是一个很小的代价。至少你知道去哪里看!您不必四处寻找数十种功能即可找到所需的文件。确定您创建的每个新文件的放置位置需要 0 秒。

Webpack的别名

Webpack 功能允许我们创建别名,指向特定文件或目录的全局名称。例如:

// 改变前
import { sortCategories } from '../../helpers/category.helpers';
// …改变之后:
import { sortCategories } from '@helpers/category.helpers';

以下是它的工作原理: 我创建了一个名为的别名@helpers,它将指向该目录/src/helpers。每当引入看到 @helpers时,它都会将该字符串替换为该目录的相对路径。

主要好处是它将相对路径 (../../helpers转换为绝对路径@helpers``../)。我从来不需要考虑需要多少个级别。当我移动文件时,我不必修复/更新任何导入路径。

实现 Webpack 别名超出了这篇博文的范围,并且会因使用的元框架而异,但您可以在 Webpack 文档中了解更多信息。

所以,这就是我构建 React 应用程序的方式!

全文完。

谢谢!

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天

点击查看活动详情