React文件夹结构的5个步骤(附代码示例)

955 阅读13分钟

如何将大型React应用结构化为文件夹和文件是一个意见很大的话题。我在写这个话题时挣扎了一段时间,因为没有正确的方法。然而,每隔一周就有人问我如何架构我的React项目--从小型到大型React项目的文件夹结构。

在实施了几年的React应用之后,我想给你分析一下我是如何为我的个人项目、自由职业者项目和React研讨会处理这件事的。这只需要5个步骤,你决定什么对你有意义,你想把它推到什么程度。所以,让我们开始吧。

对于那些说 "我到处移动文件,直到感觉合适为止 "的人。作为一个单独的开发者,这可能是没问题的,但在一个由4个开发者组成的跨功能团队中,你真的会这样做吗?在更高规模的团队中,"在没有明确愿景的情况下随便移动文件 "变得很棘手。此外,当我的咨询客户问我这个问题时,这也是我无法告诉他们的。因此,对于那些想了解这个问题的人来说,可以把这个演练作为参考指南。

单个React文件

第一步遵循的规则是。用一个文件来统治他们所有人。大多数React项目从一个src/文件夹和一个带有App组件的src/App.js文件开始。至少这是你在使用create-react-app时得到的东西。这是一个功能组件,只是渲染JSX。

import * as React from 'react';

const App = () => {
  const title = 'React';

  return (
    <div>
      <h1>Hello {title}</h1>
    </div>
  );
}

export default App;

最终,这个组件增加了更多的功能,它的规模自然会增长,需要提取其中的部分作为独立的React组件。在这里,我们正在从App组件中提取一个React列表组件与另一个子组件。

import * as React from 'react';

const list = [
  {
    id: 'a',
    firstname: 'Robin',
    lastname: 'Wieruch',
    year: 1988,
  },
  {
    id: 'b',
    firstname: 'Dave',
    lastname: 'Davidds',
    year: 1990,
  },
];

const App = () => <List list={list} />;

const List = ({ list }) => (
  <ul>
    {list.map(item => (
      <ListItem key={item.id} item={item} />
    ))}
  </ul>
);

const ListItem = ({ item }) => (
  <li>
    <div>{item.id}</div>
    <div>{item.firstname}</div>
    <div>{item.lastname}</div>
    <div>{item.year}</div>
  </li>
);

每当你开始一个新的React项目时,我都会告诉人们,在一个文件中拥有多个组件是没有问题的。在一个较大的React应用中,只要一个组件与另一个组件严格紧贴,这甚至是可以容忍的。然而,在这种情况下,最终这一个文件对你的React项目来说已经不够用了。这就是我们过渡到第二步的时候。

多个React文件

第二步遵循的规则是。多个文件来统治它们。以我们之前的App组件及其List和ListItem组件为例。与其把所有东西都放在一个src/App.js文件中,我们可以把这些组件分成多个文件。在这里,你可以决定你想走多远。例如,我将采用以下的文件夹结构。

- src/
--- App.js
--- List.js

虽然src/List.js文件会有List和ListItem组件的实现细节,但它只会从文件中导出List组件作为这个文件的公共API。

const List = ({ list }) => (
  <ul>
    {list.map(item => (
      <ListItem key={item.id} item={item} />
    ))}
  </ul>
);

const ListItem = ({ item }) => (
  <li>
    <div>{item.id}</div>
    <div>{item.firstname}</div>
    <div>{item.lastname}</div>
    <div>{item.year}</div>
  </li>
);

export { List };

接下来,src/App.js文件可以导入List组件并使用它:

import * as React from 'react';

import { List } from './List';

const list = [ ... ];

const App = () => <List list={list} />;

如果你想更进一步,你也可以将ListItem组件提取到自己的文件中,让List组件导入ListItem组件:

- src/
--- App.js
--- List.js
--- ListItem.js

然而,如前所述,这可能走得太远了,因为此时ListItem组件与List组件是紧密耦合的,因此将其留在src/List.js文件中是可以的。我遵循的经验法则是,只要一个React组件成为一个可重用的React组件,我就会把它拆成一个独立的文件,就像我们对List组件所做的那样,以使其他React组件能够访问它。

从React文件到React文件夹

从这里开始,它变得更加有趣,但也更加有主见。每个React组件最终都会变得越来越复杂。不仅是因为添加了更多的逻辑(例如,更多的JSX与条件渲染React Hooks事件处理程序的逻辑),而且还因为有更多的技术问题,如样式和测试。一个天真的方法是在每个React组件旁边添加更多的文件。例如,我们假设每个React组件都有一个测试和一个样式文件。

- src/
--- App.js
--- App.test.js
--- App.css
--- List.js
--- List.test.js
--- List.css

人们已经可以看到,这并不能很好地扩展,因为在*src/*文件夹中每增加一个组件,我们就会失去更多对每个单独组件的关注。这就是为什么我喜欢为每个React组件建立一个文件夹。

- src/
--- App/
----- index.js
----- component.js
----- test.js
----- style.css
--- List/
----- index.js
----- component.js
----- test.js
----- style.css

新的style和test文件分别实现了每个本地组件的样式和测试,而新的component.js文件则保存了组件的实际实现逻辑。缺少的是新的index.js文件,它代表文件夹的公共接口,所有与外界相关的东西都被导出。例如,对于List组件来说,它通常是这样的。

export * from './List';

App组件在其component.js文件中仍然可以通过以下方式导入List组件。

import { List } from '../List/index.js';

在JavaScript中,我们可以省略导入的*/index.js*,因为它是默认的。

import { List } from '../List';

这些文件的命名已经有了意见。例如,如果需要文件的复数化,test.js可以变成spec.jsstyle.css可以变成style.css。此外,如果你使用的不是CSS,而是像Styled Components这样的东西,你的文件扩展名也可以从style.css变为style.js

一旦你习惯了这种文件夹和文件的命名方式,你就可以在IDE中搜索 "List component "或 "App test "来打开每个文件。在这里,我承认,与我个人喜欢简洁的文件名相反,人们往往喜欢在文件名上更多的言辞。

- src/
--- App/
----- index.js
----- App.js
----- App.test.js
----- App.style.css
--- List/
----- index.js
----- List.js
----- List.test.js
----- List.style.css

总之,如果你把所有的组件文件夹都折叠起来,不管文件名是什么,你就有了一个非常简明清晰的文件夹结构。

- src/
--- App/
--- List/

如果对一个组件有更多的技术关注,例如你可能想把自定义钩子、类型(如TypeScript定义的类型)、故事(如Storybook)、实用工具(如辅助函数)或常量(如JavaScript常量)提取到专门的文件中,你可以在组件文件夹中横向扩展这种方法。

- src/
--- App/
----- index.js
----- component.js
----- test.js
----- style.css
----- types.js
--- List/
----- index.js
----- component.js
----- test.js
----- style.css
----- hooks.js
----- story.js
----- types.js
----- utils.js
----- constants.js

如果你决定通过在自己的文件中提取ListItem组件来保持你的List*/component.js*更加轻量级,那么你可能想尝试以下文件夹结构。

- src/
--- App/
----- index.js
----- component.js
----- test.js
----- style.css
--- List/
----- index.js
----- component.js
----- test.js
----- style.css
----- ListItem.js

在这里,你可以再进一步,给组件一个自己的嵌套文件夹,与所有其他技术问题如测试和样式一起。

- src/
--- App/
----- index.js
----- component.js
----- test.js
----- style.css
--- List/
----- index.js
----- component.js
----- test.js
----- style.css
----- ListItem/
------- index.js
------- component.js
------- test.js
------- style.css

重要的是。从这里开始,你需要注意不要让你的组件相互之间嵌套得太深。我的经验法则是,我永远不会将组件嵌套到两层以上,所以List和ListItem的文件夹现在这样就可以了,但ListItem的文件夹不应该有另一个嵌套的文件夹。不过,例外情况证明了这一规则。

毕竟,如果你不打算超过中型React项目,在我看来,这才是结构你的React组件的方式。根据我作为React自由职业者的经验,许多React项目都遵循这种React应用程序的组织方式。

技术文件夹

下一步将帮助你构建中型到大型的React应用程序。它将React组件与可重用的React工具(如钩子和上下文)分开,但也没有React相关的工具,如辅助函数(这里是services/)。以下面这个文件夹结构的基线为例。

- src/
--- components/
----- App/
------- index.js
------- component.js
------- test.js
------- style.css
----- List/
------- index.js
------- component.js
------- test.js
------- style.css

所有以前的React组件都被分组到一个新的*components/*文件夹中。这给我们提供了另一个垂直层,用于为其他技术类别创建文件夹。例如,在某些时候,你可能有可重用的React钩子,可以被一个以上的组件使用。因此,与其把一个自定义钩子紧紧地耦合到一个组件上,你可以把它的实现放在一个专门的文件夹里,可以被所有的React组件使用。

- src/
--- components/
----- App/
------- index.js
------- component.js
------- test.js
------- style.css
----- List/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside.js
----- useScrollDetect.js

但这并不意味着所有的钩子都应该在这个文件夹中结束。只被一个组件使用的React钩子应该留在该组件的文件中,或者在组件的文件夹中,在该组件旁边有一个hooks.js文件。只有可重用的钩子才会在新的*hooks/*文件夹中结束。如果一个钩子需要更多的文件,你可以再把它改成一个文件夹。

- src/
--- components/
----- App/
------- index.js
------- component.js
------- test.js
------- style.css
----- List/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside/
------- index.js
------- hook.js
------- test.js
----- useScrollDetect/
------- index.js
------- hook.js
------- test.js

如果你在React项目中使用React Context,同样的策略可能适用。因为上下文需要在某个地方被实例化,为它建立一个专门的文件夹/文件是最好的做法,因为它最终需要被许多React组件访问。

- src/
--- components/
----- App/
------- index.js
------- component.js
------- test.js
------- style.css
----- List/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside.js
----- useScrollDetect.js
--- context/
----- Session.js

从这里开始,可能会有其他的实用程序需要从你的components/文件夹中访问,也需要从其他新的文件夹中访问,比如hooks/context/。对于杂七杂八的实用程序,我通常创建一个*services/*文件夹。名称由你决定(例如,*utils/*是我经常看到的另一个文件夹名称,但services对下面的导入策略更有意义)。但还是那句话,使逻辑对我们项目中的其他代码可用的原则推动了这种技术分离。

- src/
--- components/
----- App/
------- index.js
------- component.js
------- test.js
------- style.css
----- List/
------- index.js
------- component.js
------- test.js
------- style.css
--- hooks/
----- useClickOutside.js
----- useScrollDetect.js
--- context/
----- Session.js
--- services/
----- ErrorTracking/
------- index.js
------- service.js
------- test.js
----- Format/
------- Date/
--------- index.js
--------- service.js
--------- test.js
------- Currency/
--------- index.js
--------- service.js
--------- test.js

Date/index.js文件为例。实现的细节可能如下所示:

export const formatDateTime = (date) =>
  new Intl.DateTimeFormat('en-US', {
    year: 'numeric',
    month: 'numeric',
    day: 'numeric',
    hour: 'numeric',
    minute: 'numeric',
    second: 'numeric',
    hour12: false,
  }).format(date);

export const formatMonth = (date) =>
  new Intl.DateTimeFormat('en-US', {
    month: 'long',
  }).format(date);

幸运的是,JavaScript的International API为我们提供了很好的日期转换工具。然而,我不喜欢在我的React组件中直接使用API,而是为它提供一个服务,因为只有这样,我才能保证我的组件只有一小部分积极使用的日期格式化选项可供应用。

现在,我们不仅可以单独导入每个日期格式化功能。

import { formatMonth } from '../../services/format/date';
const month = formatMonth(new Date());

而且还可以作为一个服务,作为一个封装的模块,换句话说,我通常喜欢这样做。

import * as dateService from '../../services/format/date';
const month = dateService.formatMonth(new Date());

现在导入相对路径的东西可能变得很困难。因此,我总是会选择使用Babel的模块解析器来获取别名。之后,你的导入可能看起来像下面这样。

import * as dateService from 'format/date';
const month = dateService.formatMonth(new Date());

毕竟,我喜欢这种技术上的分离,因为它给每个文件夹一个专门的目的,它鼓励在整个React应用程序中共享功能。

功能文件夹

最后一步将帮助你构建大型React应用程序,因为它将特定的功能相关的组件与通用的UI组件分开。前者在React项目中通常只使用一次,而后者是被多个组件使用的UI组件。

在这里我将重点讨论组件,为了保持例子的小规模,然而,同样的学习可以应用于上一节的其他技术文件夹。以下面的文件夹结构为例,它可能没有显示出问题的全部范围,但我希望你能明白这一点。

- src/
--- components/
----- App/
----- List/
----- Input/
----- Button/
----- Checkbox/
----- Radio/
----- Dropdown/
----- Profile/
----- Avatar/
----- MessageItem/
----- MessageList/
----- PaymentForm/
----- PaymentWizard/
----- ErrorMessage/
----- ErrorBoundary/

的意思。在你的*component/*中最终会有太多的组件。其中一些是可重复使用的(如Button),而另一些则与功能有关(如Message)。

从这里开始,我将只把*components/*文件夹用于可重用的组件(如 UI 组件)。其他所有的组件都应该移到各自的功能文件夹中。文件夹的名称也由你决定。

- src/
--- feature/
----- User/
------- Profile/
------- Avatar/
----- Message/
------- MessageItem/
------- MessageList/
----- Payment/
------- PaymentForm/
------- PaymentWizard/
----- Error/
------- ErrorMessage/
------- ErrorBoundary/
--- components/
----- App/
----- List/
----- Input/
----- Button/
----- Checkbox/
----- Radio/
----- Dropdown/

如果其中一个特性组件(如MessageItem、PaymentForm)需要访问共享的CheckboxRadioDropdown组件,它将从可重用的UI组件文件夹中导入。如果一个特定领域的MessageList组件需要一个抽象的List组件,它也会将其导入。

此外,如果上一节中的一个服务与某个功能紧密耦合,那么就把该服务移到特定的功能文件夹中。同样的情况也可能适用于之前因技术问题而分离的其他文件夹。

- src/
--- feature/
----- User/
------- Profile/
------- Avatar/
----- Message/
------- MessageItem/
------- MessageList/
----- Payment/
------- PaymentForm/
------- PaymentWizard/
------- services/
--------- Currency/
----------- index.js
----------- service.js
----------- test.js
----- Error/
------- ErrorMessage/
------- ErrorBoundary/
------- services/
--------- ErrorTracking/
----------- index.js
----------- service.js
----------- test.js
--- components/
--- hooks/
--- context/
--- services/
----- Format/
------- Date/
--------- index.js
--------- service.js
--------- test.js

每个特性文件夹中是否应该有一个中间的*服务/文件夹,这取决于你。你也可以不设这个文件夹,直接把ErrorTracking/文件夹放到Error/*中。然而,这可能会引起混淆,因为ErrorTracking应该以某种方式被标记为一个服务,而不是一个React组件。

这里有很多空间供你个人发挥。毕竟,这一步只是把功能集中在一起,这允许你公司的团队在特定的功能上工作,而不必在整个项目中接触文件。

奖励:文件夹/文件命名规则

在我们拥有像React.js这样的基于组件的UI库之前,我们习惯于用羊肉串大小写的命名规则来命名我们所有的文件夹和文件。在Node.js世界中,这仍然是现状的命名惯例。然而,在有基于组件的用户界面库的前端,对于包含组件的文件夹/文件,这种命名惯例变为PascalCase,因为当声明一个组件时,也遵循PascalCase的命名惯例。

- src/
--- feature/
----- user/
------- profile/
------- avatar/
----- message/
------- message-item/
------- message-list/
----- payment/
------- payment-form/
------- payment-wizard/
----- error/
------- error-message/
------- error-boundary/
--- components/
----- app/
----- list/
----- input/
----- button/
----- checkbox/
----- radio/
----- dropdown/

就像上面的例子一样,在一个完美的世界里,我们会对所有的文件夹和文件使用kebab大小写的命名惯例,因为PascalCase命名的文件夹/文件在操作系统的多样性中处理方式不同,这可能会导致与使用不同操作系统的团队合作时出现bug。

奖励:Next.js项目结构

一个Next.js项目以一个*pages/文件夹开始。这里有一个常见的问题。把src/*文件夹放在哪里?

- api/- pages/- src/--- feature/--- components/

通常情况下,源文件夹会在pages文件夹旁边创建。从这里开始,你可以按照之前讨论过的文件夹/文件结构在*src/文件夹内进行操作。我听说Next.js中有一个逃生通道,你也可以把pages/文件夹放在src/*文件夹中。

- api/- src/--- pages/--- feature/--- components/

但是,在这种情况下,不允许再有*pages/*文件夹。


写了这么多,我希望能帮助某个人或团队构建他们的React项目。请记住,所显示的方法中没有一个是固定的。相反,我鼓励你在其中运用你的个人风格。由于每个React项目的规模都会随着时间的推移而增长,大多数的文件夹结构也会非常自然地演变。因此,如果事情失去控制,5个步骤的过程可以给你一些指导。