构建React应用

我们今天发布的翻译材料揭示了结构化React应用程序时作者使用的方法。 特别是,我们将在此处讨论所使用的文件夹结构,实体的命名,测试文件的位置以及其他类似内容。

React最令人愉快的功能之一是该库不会强迫开发人员严格遵守有关项目结构的某些约定。 这大部分仍由程序员自行决定。 例如,这种方法不同于Ember.js或Angular框架中采用的方法。 它们为开发人员提供了更多标准功能。 这些框架提供了有关项目结构的约定以及用于命名文件和组件的规则。



就个人而言,我喜欢React所采用的方法。 事实是,我宁愿自己控制某些事情,而不必依赖某些“协议”。 但是,Angular提供的项目结构方法有很多优点。 自由和或多或少的僵化规则之间的选择取决于您和您的团队之间的距离。

在使用React的多年中,我尝试了许多不同的方法来构建应用程序。 我采用的某些想法比其他想法更成功。 因此,在这里,我将讨论在实践中表现良好的所有内容。 希望您在这里找到对您有用的东西。

我并不是要在这里展示一些“唯一正确的”结构化应用程序方式。 您可以采纳我的一些想法,并根据自己的需求进行更改。 您可能会像以前一样继续工作,从而很不同意我。 不同的团队创建不同的应用程序,并使用不同的方法来实现其目标。

重要的是要注意,如果您查看我参与开发的Thread网站,并查看其界面的设备,您会发现我在此谈论的那些规则没有得到遵守。 事实是,编程中的任何“规则”都应仅作为建议,而不应作为在任何情况下均有效的全面标准。 而且,如果您认为某种“规则”不适合您,那么,为了提高工作质量,您应该找到背离这些“规则”的力量。

实际上,现在,不用多说,我为您提供了有关构建React应用程序的故事。

不必太担心规则。


也许您认为在我们的谈话开始时,您对规则不必太担心的建议看起来很奇怪。 但这正是我说程序员在遵守规则方面的主要错误时所表达的意思,即程序员对规则过于重视。 在开始新项目时尤其如此。 在创建第一个index.jsx根本不可能知道什么是该项目的最佳选择。 随着项目的发展,您自然会遇到某种文件和文件夹结构,这对本项目可能非常有用。 如果在继续进行过程中发现现有结构在某种程度上不成功,则可以对其进行改进。

如果您阅读了此书,并发现自己在讨论应用程序中没有任何内容,那么这不是问题。 每个应用程序都是唯一的,没有两个完全相同的开发团队。 因此,每个从事项目的团队都会就其结构和工作方法达成一些协议。 这有助于团队成员高效地工作。 在了解某人的行为后,请不要努力将其立即介绍给自己。 不要试图在您的工作中引入某些材料中所谓的内容,即使这样,也不能解决问题的“最有效方法”。 对于这些建议,我一直坚持并遵循以下策略。 我有自己的一套规则,但是在阅读有关其他人在某些情况下的行为时,我会选择对我而言似乎成功且适合我的事物。 这导致这样的事实,随着时间的流逝,我的工作方法得到了改善。 同时,我没有任何震惊,也没有从头开始重写所有内容的愿望。

重要组件位于单独的文件夹中


将组件文件放置在文件夹中的方法是,将那些在应用程序上下文中被视为“重要”,“基本”,“基本”的组件放置在单独的文件夹中。 这些文件夹又位于components文件夹中。 例如,如果我们在谈论一家电子商店的应用程序,则用于描述产品的<Product>组件可以被识别为相似的组件。 这是我的意思:

 - src/  - components/    - product/      - product.jsx      - product-price.jsx    - navigation/      - navigation.jsx    - checkout-flow/      - checkout-flow.jsx 

在这种情况下,仅由某些“主要”组件使用的“辅助”组件与这些“主要”组件位于同一文件夹中。 这种方法已经在实践中证明了自己。 事实是,由于其应用程序,项目中会出现某种结构,但是文件夹嵌套的级别不会太大。 它的应用不会导致在组件导入命令../../../出现../../../之类的东西,也不会使在项目中移动变得困难。 这种方法使您可以构建清晰的组件层次结构。 该名称与文件夹名称匹配的组件被视为“基本”。 位于同一文件夹中的其他组件用于将“基础”组件划分为多个部分,从而简化了该组件及其支持代码的使用。

尽管我支持项目中存在某种文件夹结构,但我认为最重要的是选择好的文件名。 文件夹本身不太重要。

对子组件使用子文件夹


上述方法的缺点之一是其使用可能导致包含大量文件的“基本”组件的文件夹出现。 例如,考虑<Product>组件。 CSS文件将附加到该文件(我们将在后面讨论),测试文件,许多子组件以及可能的其他资源-例如图像和SVG图标。 此“附加项”列表不受限制。 所有这些都将落入与“基本”组件相同的文件夹中。

我真的不在乎。 如果文件具有经过深思熟虑的文件名,并且可以轻松找到它们(使用编辑器中的文件搜索工具),这对我来说很合适。 如果是这样,则文件夹结构会淡入背景。 这是有关此主题推文。

但是,如果您希望项目具有更广泛的结构,则将子组件移动到其自己的文件夹没有什么困难:

 - src/  - components/    - product/      - product.jsx      - ...      - product-price/        - product-price.jsx 

测试文件与被测组件的文件位于同一位置。


我们从一个简单的建议开始本节,即建议将测试文件与带有在帮助下检查其代码的文件放置在同一位置。 我还将讨论如何更好地构造组件,以确保它们彼此靠近。 但是现在我可以说,我发现将测试文件与组件文件放在同一文件夹中很方便。 在这种情况下,带有测试的文件的名称与带有代码的文件的名称相同。 在文件扩展名之前,仅将后缀.test添加到测试名称中:

  • 组件文件名: auth.js
  • 测试文件名: auth.test.js

这种方法具有以下优点:

  • 它使查找测试文件变得容易。 一目了然,您可以了解我是否正在使用该组件进行测试。
  • 所有必需的导入命令都非常简单。 在测试中,要导入测试的代码,无需创建描述__tests__文件夹退出的__tests__ 。 这样的团队看起来非常简单。 例如,如下所示: import Auth from './auth'

如果我们在测试期间使用了一些数据,例如API请求模拟之类的数据,我们会将它们放在组件及其测试已经存在的同一文件夹中。 当所有可能需要的东西都放在一个文件夹中时,这有助于提高生产率。 例如,如果您使用分支文件夹结构,并且程序员确定某个文件存在,但是不记得其名称,则程序员将不得不在许多子目录中查找该文件。 使用建议的方法,只需查看一个文件夹的内容,一切将变得清晰。

CSS模块


我非常喜欢CSS模块 。 我们发现它们非常适合为组件创建模块化CSS规则。

另外,我真的很喜欢样式化组件技术。 但是,在许多开发人员参与的项目的工作过程中,事实证明,项目中存在真实的CSS文件会提高可用性。

您可能已经猜到了,我们的CSS文件与其他文件一样,位于组件文件的旁边,位于同一文件夹中。 当您需要快速了解类的含义时,这极大地简化了文件之间的移动。

更为笼统的建议(其实质贯穿于所有这些材料)是,与某个组件相关的所有代码应保存在该组件所在的同一文件夹中。 使用单独的文件夹存储CSS和JS代码,测试代码和其他资源的日子已经一去不复返了。 使用复杂的文件夹结构会使文件之间的移动复杂化,并且没有任何明显的好处,只是它有助于“组织代码”。 将互连的文件放在同一文件夹中-这意味着在工作期间在文件夹之间移动的时间更少。

我们甚至为CSS创建了Webpack加载器,其功能与我们的工作功能相对应。 如果我们引用的类不存在,它将检查已声明的类名,并在控制台中引发错误。

几乎总是将一个组件代码放在一个文件中


我的经验表明,程序员通常过于严格地遵循一个规则,即一个文件中只有一个React组件的代码应该在一个文件中。 同时,我完全支持不值得在一个文件中放置太多组件的想法(请想象一下命名此类文件的困难!)。 但是我相信将某个“大”组件的代码和与之关联的“小”组件的代码放在同一个文件中并没有错。 如果这样的举动有助于保持代码的纯度,如果“小”组件太大而无法将其放在单独的文件中,那么这不会损害任何人。

例如,如果我创建一个<Product>组件,并且需要一小段代码来显示价格,那么我可以这样做:

 const Price = ({ price, currency }) => (  <span>    {currency}    {formatPrice(price)}  </span> ) const Product = props => {  // ,      !  return (    <div>      <Price price={props.price} currency={props.currency} />      <div>loads more stuff...</div>    </div>  ) } 

这种方法的好处是,我不必为<Price>组件创建单独的文件,并且<Product>组件只能使用该组件。 我们不导出此组件,因此无法将其导入应用程序中的其他位置。 这意味着当询问是否将<Price>放在单独的文件中时,如果需要将其导入其他位置,则可以给出明确的肯定答案。 否则,您可以不必将<Price>代码放入单独的文件中。

通用组件的单独文件夹


我们最近一直在使用通用组件。 实际上,它们构成了我们的设计系统(计划在某一天发布),但是到目前为止,我们还是从小开始-带有<Button><Logo> 。 如果组件未绑定到站点的特定部分,而是用户界面的组成部分之一,则该组件被视为“通用”。

相似的组件位于您自己的文件夹中( src/components/generic )。 这极大地简化了所有通用组件的工作。 他们在一个地方-非常方便。 随着时间的推移,随着项目的发展,我们计划开发样式指南(我们是react-styleguidist的忠实拥护者 ),以进一步简化通用组件的工作。

使用别名导入实体


我们项目中相对平坦的文件夹结构可确保导入命令的结构不会太长,例如../../ 。 但是没有它们很难做到。 因此,我们使用babel-plugin-module-resolver来配置别名以简化导入命令。

您可以使用Webpack进行相同的操作,但是多亏了Babel插件,相同的导入命令也可以在测试中工作。

我们为它配置了一对别名:

 {  components: './src/components',  '^generic/([\\w_]+)': './src/components/generic/\\1/\\1', } 

第一个非常简单。 它允许您导入任何组件,并以单词components开头命令。 在通常的方法中,导入命令如下所示:

 import Product from '../../components/product/product' 

相反,我们可以这样编写它们:

 import Product from 'components/product/product' 

这两个命令均导入同一文件。 这非常方便,因为它使您不必考虑文件夹结构。

第二个别名稍微复杂一点:

 '^generic/([\\w_]+)': './src/components/generic/\\1/\\1', 

我们在这里使用正则表达式。 它会找到以generic开头的导入命令(表达式开头的^符号允许您仅选择那些以generic开头的命令),并将generic/之后的内容捕获到组中。 之后,我们在./src/components/generic/\\1/\\1构造中使用捕获的片段( \\1 )。

结果,我们可以对此类通用组件使用导入命令:

 import Button from 'generic/button' 

它们被转换为以下命令:

 import Button from 'src/components/generic/button/button' 

例如,此命令用于导入描述通用按钮的JSX文件。 我们这样做是因为这种方法大大简化了通用组件的导入。 另外,如果我们决定更改项目文件的结构,它将对我们有好处(随着我们设计系统的发展,这很有可能)。

在这里,我想指出,使用假名时应格外小心。 如果您只有少数几个,并且它们旨在解决标准的导入问题,那么一切都很好。 但是,如果您有很多,它们带来的混乱会多于好处。

实用程序的通用lib文件夹


我想重新找回所有花时间去寻找不是组件代码的理想位置。 我根据不同的原则分享了所有这些内容,重点介绍了实用程序,服务和辅助功能的代码。 所有这些都有太多名称,我不会一一列举。 现在,我不是要找出“实用程序”和“辅助功能”之间的区别,以便为某个文件找到正确的位置。 现在,我使用一种更简单,更易理解的方法:所有这些都放在一个lib文件夹中。

从长远来看,该文件夹的大小可能太大了,您必须以某种方式对其进行结构化,但这是完全正常的。 为某些事物配备一定的结构总是比摆脱过多的结构化错误要容易得多。

在我们的Thread项目中, lib文件夹包含大约100个文件。 它们大致相等地分为包含某些功能的实现的文件和测试文件。 查找必要的文件没有任何困难。 由于大多数编辑器都内置了智能搜索lib/name_of_thing ,因此我几乎总是必须输入lib/name_of_thing类的东西,然后才找到我需要的东西。

此外,我们还有一个别名,可以简化从lib文件夹的导入,使您可以使用以下类型的命令:

 import formatPrice from 'lib/format_price' 

平面文件夹结构不要惊慌,它可能导致多个文件存储在一个文件夹中。 通常,这样的结构是某个项目所需的全部。

隐藏本机API背后的第三方库


我真的很喜欢Sentry漏洞监控系统。 在开发应用程序的服务器和客户端部分时,我经常使用它。 借助其帮助,您可以捕获异常并接收有关异常发生的通知。 这是一个很棒的工具,可让我们及时了解网站上遇到的问题。

每当我在项目中使用第三方库时,我都会考虑如何制作它,以便在必要时可以轻松地用其他东西替换它。 通常,与我们真正喜欢的相同Sentry系统一样,这不是必需的。 但是,以防万一,想出一种避免使用某种服务或将其更改为其他服务的方法不会有任何伤害。

解决此问题的最佳方法是开发自己的API,以隐藏其他人的工具。 这类似于创建一个lib/error-reporting.js模块,该模块导出reportError()函数。 该模块的核心使用Sentry。 但是Sentry仅直接导入此模块中,而没有其他地方。 这意味着用其他工具替换Sentry看起来非常简单。 为此,只需在一处更改一个文件就足够了。 只要此文件的公共API保持不变,项目的其余部分甚至在调用reportError()时都不知道使用的不是Sentry,而是其他。

请注意,模块的公共API称为模块导出的函数及其参数。 它们也称为模块的公共接口。

使用PropTypes(或诸如TypeScript或Flow之类的工具)


在进行编程时,我会想到自己的三个版本:

  • 杰克的过去和他编写的代码(有时是可疑的代码)。
  • 今天的杰克,以及他现在编写的代码。
  • 未来的杰克。 当我自己考虑这个未来时,我问自己现在的事情,该如何编写使将来的生活更轻松的代码。

听起来可能很奇怪,但我发现它很有用,考虑如何编写代码,并提出以下问题:“六个月后会如何看待它?”。

一种使自己呈现并提高工作效率的简单方法是指定组件使用的属性类型( PropTypes )。 这样可以节省搜索可能的错字的时间。 这样可以防止您在使用组件时应用了错误类型的属性,或者完全忘记了属性的转移。 在我们的案例中, eslint-react / prop-types规则很好地提醒了使用PropTypes

如果您走得更远,建议尽可能准确地描述属性。 例如,您可以这样做:

 blogPost: PropTypes.object.isRequired 

但是这样做会更好:

 blogPost: PropTypes.shape({  id: PropTypes.number.isRequired,  title: PropTypes.string.isRequired,  //    }).isRequired 

在第一个示例中,执行了最低限度的检查。 第二,向开发人员提供了更多有用的信息。 例如,如果有人忘记了对象中使用的某个字段,它们将非常方便。

第三方库仅在确实需要时才使用。


随着React钩子的出现,这个技巧比以往任何时候都更有意义。 例如,我对Thread网站的其中一部分进行了较大的改动,因此决定特别注意第三方库的使用。 , , . ( ), . , React-. — , , React API Context, .

, , Redux, . , ( , ). , , , .


— , , . .

 //     emitter.send('user_add_to_cart') //     emitter.on('user_add_to_cart', () => {  //  -  }) 

, . , . , « ». , , , . . «» - , . , , .

Redux. . , . , , user_add_to_cart , . . , , Redux, . , Redux , .

, , , , :

  • , , . .
  • , , . , .
  • , - , . , , .

- , . , , . , , , «» .

, , API Context - .


, (, , ). : , .

, , , . :

 const wrapper = mount(  <UserAuth.Provider value=>    <ComponentUnderTest />  </UserAuth.Provider> ) 

:

 const wrapper = mountWithAuth(ComponentUnderTest, {  name: 'Jack',  userId: 1, }) 

:

  • . — , , , .
  • mountWithAuth . , .

, test-utils.js . , — . .

总结


. , , , , . , , . , : . . - , .

亲爱的读者们! React-?

Source: https://habr.com/ru/post/zh-CN460793/


All Articles