前言
建个文件、目录简单,但是你有没有考虑过,如何优雅的构建前端项目的文件和目录?
建立的文件和目录显得专业化,让人一看就称呼你为大牛。
在过去开发的境遇里,我尝试了很多不同的方法,并且我已经迭代了我非常满意的解决方案。
在这篇文章中,我将分享我在当前所有项目中使用的结构,文章以 React 项目为例子介绍, 同样你也可以在 Vue 项目中使用,两者之间并不会有太大的差距,
同样你也不必因为是React 而觉得毫无收获
文件目录展示
需求分析
第一:我想使导入组件变得容易。
我希望能够写这个:
import Button from '../Button';
// 使用Webpack别名
import Button from '@components/Button';
第二:拥有漂亮、干净的组件文件名
当我在 IDE 中工作时,我不想顶部打开的文件栏被淹没。例如顶部栏如下所示的项目中都是打开的:index.js
公平地说,当同时打开多个文件时,大多数编辑器现在都会包含父目录,但每个选项卡会占用更多空间:index.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.js、index.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 策略
好的,所以你会注意到我选择在这里做两件事:
- 我用'-'连字符代替小驼峰的写法。
- 我每个文件名的末尾添加
.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 天