51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

如何组织大型React应用并使其组件模块化

在本文中,我将讨论构建和构造大型React应用程序时采用的方法。React的最佳功能之一是它如何摆脱阻碍,并且在文件结构方面几乎没有描述性。因此,您会在Stack Overflow和类似站点上发现很多问题,询问如何构建应用程序。这是一个很自以为是的话题,没有正确的方法。在本文中,我将向您介绍在构建React应用程序时所做出的决策:挑选工具,结构化文件以及将组件分解成小块。

aaa.jpg

生成工具和整理

一旦进入webpack并掌握了概念,您将真正拥有不可思议的强大力量。我使用Babel来编译我的代码,包括React特定的转换(如JSX)和webpack-dev-server来本地服务我的站点。我个人还没有发现热重装可以给我带来很多好处,所以我对webpack-dev-server及其页面的自动刷新感到非常满意。

我使用ES 2015中首次引入的ES模块(通过Babel转译)来导入和导出依赖项。这种语法已经存在了一段时间,尽管webpack可以支持CommonJS(又名Node风格的导入),但对于我来说,开始使用最新的语法是有意义的。此外,webpack可以使用ES2015模块从捆绑中删除无效代码,这虽然并不完美,但却是非常方便的功能,随着社区朝着在ES2015中向npm发布代码的方向发展,这一功能将变得更加有益。大多数网络生态系统已转向ES模块,因此对于我开始的每个新项目来说,这都是显而易见的选择。如果您不想使用webpack ,大多数工具也希望它能够支持这些工具,包括Rollup等其他捆绑软件。

文件夹结构

对于所有React应用程序,没有一个正确的文件夹结构。(与本文的其余部分一样,您应根据自己的喜好对其进行更改。)但是以下内容对我来说很有效。

代码存在于 src

为了使事情井井有条,我将所有应用程序代码放在一个名为的文件夹中src。它仅包含以最终捆绑包结尾的代码,仅此而已。这很有用,因为您可以告诉Babel(或对您的应用程序代码起作用的任何其他工具)仅查看一个目录,并确保它不会处理不需要的任何代码。其他代码,例如webpack配置文件,位于适当命名的文件夹中。例如,我的顶级文件夹结构通常包含:

- src => app code here
- webpack => webpack configs
- scripts => any build scripts
- tests => any test specific code (API mocks, etc.)

通常情况下,这将是在顶层的唯一文件是index.htmlpackage.json和任何点文件,如.babelrc。有些人喜欢在中包含Babel配置package.json,但是我发现那些文件在具有许多依赖项的较大项目上会变得很大,所以我喜欢使用.eslintrc.babelrc等等。

React Components

有了src文件夹后,棘手的一点是确定如何构造组件。过去,我将所有组件放在一个大文件夹中,例如src/components,但是我发现在较大的项目中,这很快就会使您不知所措。

常见的趋势是拥有用于"smart"和"dumb"组件(也称为"容器"和"代表性"组件)的文件夹,但是个人而言,我从来没有发现显式文件夹适合我。尽管我确实将组件大致分为"smart"和"dumb"组件(我将在下文中详细介绍),但每个组件都没有特定的文件夹。

我们根据应用程序的使用区域对组件进行了分组,core文件夹为整个使用的通用组件(按钮,页眉,页脚等通用且可重用的组件)。其余文件夹映射到应用程序的特定区域。例如,我们有一个名为的文件夹cart,其中包含与购物车视图有关的所有组件;还有一个名为的文件夹listings,其中包含用于列出用户可以在页面上购买的商品的代码。

归类到文件夹中还意味着您可以避免在组件所使用的应用程序区域中添加前缀。 例如,如果我们有一个可以显示用户购物车总成本的组件,一般很多人喜欢倾向于CartTotal命名,我可能更喜欢使用Total,因为我是从Cart文件夹中导入它的:

import Total from '../cart/total'
// vs
import CartTotal from '../cart/cart-total'

额外的前缀更清晰些,特别是在您有两个到三个相似命名的组件的情况下,这种技术通常可以避免名称的额外重复。

优先使用jsx大写字母扩展名

很多人使用文件中的大写字母来命名React组件,以区别于常规JavaScript文件。 因此,在上述导入中,文件将为CartTotal.js或Total.js。 我倾向于使用以短划线作为分隔符的小写文件,为了区分,我对React组件使用.jsx扩展名。 因此,所以我坚持使用cart-total.jsx。

这样做的好处很小,就是可以通过限制对的文件进行搜索,从而仅搜索React文件,.jsx甚至可以根据需要将特定的webpack插件应用于这些文件。

无论选择哪种命名约定,重要的是都要坚持下去。随着代码库的各种惯例的组合,随着它的增长,它很快将成为一场噩梦,您必须对其进行导航。您可以.jsx使用eslint-plugin-react中的规则来强制执行此约定。

每个文件一个React组件

根据上一条规则,我们遵循一个React组件文件的约定,并且该组件应始终是默认导出。

通常,我们的React文件如下所示:

import React from 'react'

export default function Total(props) {
  …
}

例如,在必须包装组件以将其连接到Redux数据存储的情况下,完全包装的组件将成为默认导出:

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'

export default function Total(props) {
  …
}

export default connect(() => {…})(Total)

您会注意到,我们仍然会导出原始组件。这对于测试非常有用,您可以在其中使用"普通"组件,而不必在单元测试中设置Redux。

通过将组件保留为默认导出,可以轻松导入组件并知道如何使用它,而不必查找确切的名称。这种方法的缺点是,导入人员可以根据需要调用该组件。再次,我们对此有一个约定:导入应以文件命名。因此,如果要导入total.jsx,则应将组件导入为Totaluser-header.jsx变为UserHeader,依此类推。

值得注意的是,并非总是遵循每个文件一个组件的规则。如果最终要构建一个小的组件来帮助您呈现部分数据,并且只在一个地方使用它,则将其与使用它的组件放在同一文件中通常会更容易。将组件保存在单独的文件中是有代价的:有更多的文件,更多的导入,并且作为开发人员通常要遵循的更多,因此请考虑是否值得。像本文中的大多数建议一样,它们是有例外的规则。

smart"和"dumb"React组件

我简要提到了smart"和"dumb"组件的分离,这是我们在代码库中坚持的。尽管我们无法通过将其拆分为文件夹来识别它,但是您可以将我们的应用大致分为两种类型的组件:

  • 处理数据,连接到Redux并处理用户交互的"smart"组件

  • 被赋予一组道具并向屏幕呈现一些数据的"dumb"组件

这些组件构成了我们应用程序的大部分,如果可能的话,您应该始终偏爱这些组件。它们更易于使用,故障更少,更易于测试。

即使必须创建"smart"组件,我们也尝试将所有JavaScript逻辑保留在自己的文件中。理想情况下,必须处理数据的组件应将数据交给一些可以处理数据的JavaScript。这样,可以将操作代码与React分开进行测试,并且可以在测试React组件时根据需要对其进行模拟。

避免大render方法

尽管这一点通常用于指代renderReact类组件上定义的方法,但在谈论功能组件时,这一点仍然存在,因为您应该注意呈现异常大的HTML片段的组件。

我们努力做到的一件事是拥有许多小的React组件,而不是更少的更大的组件。组件过大的一个很好的指导是渲染函数的大小。如果它变得笨拙,或者您需要将其拆分为许多较小的渲染函数,则可能是时候考虑抽象一个函数了。

这不是硬性规定。您和您的团队需要了解您满意的组件的大小,然后再拉出更多的组件,但是组件render功能的大小是很好的衡量标准。您也可以使用状态中的道具或物品数量作为另一个好的指标。如果某个组件要使用七个不同的道具,则可能表明它做得太多。

一律使用 prop-type

React允许您记录期望使用组件的prop-types包给予组件的属性的名称和类型。

通过声明预期道具的名称和类型,以及它们是否是可选的,您可以更有信心在使用组件时拥有正确的属性,并且如果忘记了,可以花更少的时间进行调试属性名称或给它错误的类型。您可以使用eslint-plugin-react PropTypes规则强制执行此操作。

尽管花些时间添加这些内容可能会徒劳无功,但是当您这样做时,重新使用六个月前编写的组件时,您会发现还不错。

Redux

我们还在许多应用程序中使用Redux来管理应用程序中的数据,如何构造Redux应用程序是另一个非常普遍的问题,有很多不同的见解。

对我们来说,赢家是Ducks,该提案将应用程序每个部分的动作,归约器和动作创建者放在一个文件中。同样,尽管这是为我们工作的方法,但在这里选择并遵守约定是最重要的。

Ducks系统认为,而不是具有reducers.jsactions.js,其中每个都包含彼此相关的代码,而是将相关代码组合到一个文件中更有意义。假设您有一个Redux商店,其中有两个顶级密钥userposts。您的文件夹结构如下所示:

ducks
- index.js
- user.js
- posts.js

index.js将包含创建主减速器的代码(可能使用combineReducersRedux来实现),然后在user.js其中posts.js放置所有代码,通常如下所示:

// user.js

const LOG_IN = 'LOG_IN'

export const logIn = name => ({ type: LOG_IN, name })

export default function reducer(state = {}, action) {
  …
}

这省去了从不同文件中导入动作和动作创建者的麻烦,并使商店中不同部分的代码彼此相邻。

独立JavaScript模块

尽管本文的重点是React组件,但是在构建React应用程序时,您会发现自己编写了许多与React完全分开的代码。这是我最喜欢该框架的一件事:许多代码与组件完全分离。

每当您发现组件中充满了可能被移出组件的业务逻辑时,我建议您这样做。根据我的经验,我们发现一个名为lib或的文件夹services在这里运作良好。具体名称无关紧要,但是您所需要的只是一个充满"非反应性组件"的文件夹。

这些服务有时会导出一组功能,有时会导出相关功能的对象。例如,我们有services/local-storage.js,它为本地window.localStorageAPI提供了一个小的包装:

// services/local-storage.js

const LocalStorage = {
  get() {},
  set() {},
  …
}

export default LocalStorage

将逻辑排除在这样的组件之外会带来很多好处:

  1. 您可以隔离测试此代码,而无需渲染任何React组件

  2. 在您的React组件中,您可以对服务进行存根操作并返回特定测试所需的数据

测验

如上所述,我们非常广泛地测试了我们的代码,并逐渐依赖Facebook的Jest框架作为完成工作的最佳工具。它非常快速,擅长处理大量测试,可以在监视模式下快速运行并为您提供快速反馈,并且具有一些方便的功能,可以立即测试React。之前,我已经在SitePoint上进行了广泛的介绍,因此这里不再赘述,但我将讨论如何构建测试。

过去,我致力于拥有一个单独的tests文件夹,其中包含所有测试的所有内容。因此,如果您有src/app/foo.jsx,您也会有tests/app/foo.test.jsx。实际上,随着应用程序的变大,这将使查找正确的文件变得更加困难,并且如果将文件移入src,您通常会忘记将它们移入test,并且结构不同步。另外,如果其中有一个文件tests需要导入中的文件src,则最终将导致很长的导入。我敢肯定我们都遇到了这个问题:

import Foo from '../../../src/app/foo'

如果更改目录结构,这些将很难使用,也很难修复。

相反,将每个测试文件放在其源文件旁边可以避免所有这些问题。为了区分它们,我们给测试加上了后缀.spec(尽管其他人使用.test或只是简单地加上了),-test但它们与源代码并存,否则与同名:

- cart
  - total.jsx
  - total.spec.jsx
- services
  - local-storage.js
  - local-storage.spec.js

随着文件夹结构的变化,可以轻松移动正确的测试文件,并且当文件没有任何测试时,这一点也非常明显,因此您可以发现并修复这些问题。

结论

React 框架的最佳功能之一是如何使您围绕工具,构建工具和文件夹结构做出大多数决定,而您应该接受这一决定。我希望本文为您提供了一些有关如何处理较大的React应用程序的想法,但是您应该采纳我的想法并对其进行调整,以适合您自己和团队的偏好。

赞(0)
未经允许不得转载:工具盒子 » 如何组织大型React应用并使其组件模块化