大型React应用程序的开发组织

这篇文章基于一系列关于使用React使jQuery前端现代化的系列文章。为了更好地理解撰写本文的原因,建议阅读本系列第一篇文章。 如今,组织小型React应用程序的开发或从头开始很容易。特别是在使用create-react-app时。一些项目很可能只需要几个依赖项(例如,管理应用程序状态和使项目国际化)和至少包含目录的文件夹







srccomponents...我相信这是大多数React项目开始的结构。但是,通常,随着项目依赖关系数量的增加,程序员面临着组成中包含的组件,Reducer和其他可重用机制数量增加的问题。有时,这一切都变得非常不舒服且难以管理。例如,如果现在还不清楚为什么需要某些依赖项以及它们如何组合在一起,该怎么办?或者,如果项目积累了太多的组件而又难以在其中找到合适的组件,该怎么办?如果程序员需要查找忘记了名称的某个组件怎么办?



这些只是我们在改造Karify中的前端时必须寻找答案的问题的一些示例。我们知道依赖和项目组成部分的数量可能有一天会失控。这意味着我们必须计划所有事情,以便随着项目的发展,我们可以自信地继续开展工作。该计划包括就文件和文件夹结构以及代码质量达成一致。其中包括对项目总体架构的描述。最重要的是,必须做到这一点,以便新来的项目程序员可以轻松地理解所有这些,从而使他们(包括在工作中)不必花太长时间研究项目,就可以理解项目的所有依赖关系和代码风格。



在撰写本文时,我们的项目中大约有1200个JavaScript文件。其中350个是组件。该代码已经过80%的单元测试。由于我们仍然遵守我们已经建立并在先前创建的项目体系结构框架内工作的协议,因此我们决定与所有公众分享所有这些都是很好的。这就是这篇文章的来源。在这里,我们将讨论如何组织大型React应用程序的开发,以及我们从处理该应用程序的经验中学到的教训。



如何组织文件和文件夹?



经过项目的多个阶段后,我们才找到一种方便地组织React前端材料的方法。最初,我们将把项目资料托管在存储基于jQuery的前端代码的同一存储库中。但是,由于我们使用的后端框架对项目施加的文件夹结构的要求,因此该选项对我们不起作用。接下来,我们考虑将前端代码移至单独的存储库。最初,这种方法很好用,但是随着时间的流逝,我们开始考虑创建项目的其他客户端部分,例如,基于React Native的前端。这让我们考虑了组件库。结果,我们将新的存储库分为两个单独的存储库。一个用于组件库,另一个用于新的React前端。尽管起初我们以为这个想法是成功的,但是它的实现导致代码审查过程严重复杂。我们两个存储库中的更改之间的关系尚不清楚。结果,我们决定再次切换到将代码存储在单个存储库中,但是现在它是一个单存储库。



我们选择了一个Mono仓库,因为我们想在组件库和项目中的前端应用程序之间引入隔离。 Mono存储库与其他类似存储库之间的区别在于,我们不需要在存储库内发布软件包。在我们的案例中,软件包只是确保开发模块化的一种手段,也是关注点分离的工具。为应用程序的不同变体使用不同的软件包特别有用,因为这使您可以为每个应用程序定义不同的依赖关系,并为每个应用不同的脚本。



我们使用毛线工作区在根文件中使用以下配置来建立我们的单存储库package.json



"workspaces": [
    "app/*",
    "lib/*",
    "tool/*"
]


现在,有些人可能想知道为什么我们不使用package文件夹,而与其他单一存储库一样。这主要是由于我们要分离应用程序和组件库。此外,我们知道我们需要创建一些自己的工具。结果,我们到达了上面的文件夹结构。这些文件夹在项目中的播放方式如下:



  • app:此文件夹中的所有软件包都与前端应用程序相关,例如Karify前端和一些其他内部前端。我们的故事书资料也存储在这里
  • lib: -, , . , , . , , typography, media primitive.
  • tool: , , Node.js. , , , . , , webpack, , ( « »).


我们所有的软件包,无论它们存储在哪个文件夹中,都具有一个子文件夹src,以及一个可选的folder binsrc存储在目录app和中软件包文件夹lib可能包含以下一些子文件夹:



  • actions:包含用于创建动作的函数,这些动作的返回值可以从redux传递给分派函数useReducer
  • components:包含组件的文件夹及其代码,翻译,单元测试,快照,历史记录(如果适用于特定组件)。
  • constants:此文件夹存储在不同环境中不变的值。实用程序也存储在这里。
  • fetch:在此处存储类型定义,以处理从我们的API接收的数据,以及用于接收此类数据的相应异步操作。
  • helpers: , .
  • reducers: , redux useReducer.
  • routes: , react-router history.
  • selectors: , redux-, , API.


这种文件夹结构使我们能够编写真正的模块化代码,因为它创建了一个清晰的系统,用于在依赖项定义的各个概念之间划分职责。这有助于我们在存储库中搜索变量,函数和组件,此外,无论正在寻找它们的人是否知道它们的存在。此外,它有助于我们将最少的内容保留在单独的文件夹中,从而使使用它们更容易。



当我们开始应用此文件夹结构时,我们面临着确保一致地应用这种结构的挑战。当使用不同的软件包时,开发人员可能希望在这些软件包的文件夹中创建不同的文件夹,并以不同的方式组织这些文件夹中的文件。虽然并非总是一件坏事,但这种杂乱无章的做法会导致混乱。为了帮助我们系统地应用上述结构,我们创建了可以称为“文件系统短子”的文件。我们现在将谈论这个。



您如何确保应用样式指南?



我们努力在项目中统一文件和文件夹的结构。我们想要在代码上实现相同的目的。到那时,我们已经具有解决该项目的jQuery版本中类似问题的成功经验,但是我们还有很多改进之处,尤其是在CSS方面。结果,我们决定从头开始创建样式指南,并确保将其与棉绒一起使用。代码审阅期间控制了不能由lint强制执行的规则。



在mono存储库中设置linter的方式与在其他任何存储库中相同。这很好,因为它允许您在一次运行中检出整个存储库。如果您对短绒不熟悉,建议您看一下ESLintStylelint。我们完全使用它们。



事实证明,JavaScript linter在以下情况下特别有用:



  • 确保使用考虑到内容可访问性构建的组件,而不是使用它们的HTML对应组件。在创建样式指南时,我们引入了一些有关链接,按钮,图像和图标的可访问性的规则。然后,我们需要在代码中强制执行这些规则,并确保我们将来不会忘记它们。我们这样做是使用反应/禁止元素排除eslint-plugin的反应的


这是一个看起来像的例子:



'react/forbid-elements': [
    'error',
    {
        forbid: [
            {
                element: 'img',
                message: 'Use "<Image>" instead. This is important for accessibility reasons.',
            },
        ],
    },
],






除了支持JavaScript和CSS外,我们还拥有自己的“文件系统绒毛”。是他确保统一使用我们选择的文件夹结构。由于这是我们自己创建的工具,因此,如果我们决定切换到其他文件夹结构,则可以随时对其进行相应的更改。以下是我们在处理文件和文件夹时控制的规则的示例:



  • 检查组件的文件夹结构:确保始终有一个文件index.ts和一个.tsx与该文件夹同名.file。
  • 文件验证package.json:确保每个包中只有一个这样的文件,并且private已设置该属性true以防止意外发布该包。


您应该选择哪种类型的系统?



如今,对于许多人来说,本部分标题中的问题答案可能非常简单。您只需要使用TypeScript即可。在某些情况下,无论项目大小如何,实现TypeScript都会减慢开发速度。但是我们认为,这对于提高代码的质量和严格性来说是一个合理的代价。



不幸的是,当我们开始从事该项目时,prop-types系统仍被广泛使用。...在我们开始工作之初,这对我们就足够了,但是随着项目的发展,我们开始错过了为非组件实体声明类型的能力。我们已经看到,这将帮助我们改进例如减速器和选择器。但是,将不同的键入系统引入到项目中将需要大量代码重构才能键入整个代码库。



最后,我们仍然为我们的项目配备了类型支持,但是犯了先尝试Flow的错误。... 在我们看来,Flow易于集成到项目中。尽管是这种情况,但是我们经常遇到Flow的各种问题。该系统无法与我们的IDE很好地集成,有时由于某种未知的原因而无法检测到一些错误,因此创建泛型类型确实是一场噩梦。由于这些原因,我们最终将所有内容都迁移到TypeScript。如果我们知道现在所知道的,我们将立即选择TypeScript。



由于TypeScript近年来的发展方向,这种过渡对我们来说非常容易。从TSLint到ESLint过渡对我们特别有用



如何测试代码?



当我们开始进行该项目时,我们还不清楚选择哪种测试工具。如果我现在正在考虑它,我想说的是,对于单元和集成测试,最好分别使用jestcypress。这些工具有据可查且易于使用。唯一可惜的是cypress不支持Fetch API,不好的是该工具的API没有设计为使用async / await构造。我们开始使用柏树后,并没有立即了解这一点。但我希望这种情况在不久的将来会有所改善。



起初,我们很难找到编写单元测试的最佳方法。随着时间的推移,我们尝试了一些方法,例如快照测试测试渲染器浅渲染器。我们尝试了测试库。我们最终进行了浅层渲染,用于测试组件的输出,并使用测试渲染来测试组件的内部逻辑。



我们认为测试库是小型项目的良好解决方案。但是,该系统依赖DOM渲染这一事实对基准性能产生了很大影响。此外,我们认为批评对于非常“深”的组件,使用表面渲染进行快照测试是无关紧要的。对于我们来说,快照对于检查所有可能的输出组件选项非常有用。但是,组件代码不应过于复杂;您应努力使其易于阅读。这可以toJSON通过使组件变小并为与快照无关的组件输入定义方法来实现。



为了避免忘记单元测试,我们通过测试设置代码覆盖率阈值... 开玩笑来说,这很容易做到,没什么可考虑的。仅通过测试设置全局代码覆盖率的指标就足够了。因此,在开始工作时,我们将此数字设置为60%。随着时间的流逝,随着我们代码库的测试覆盖率的增长,我们将其提高到80%。我们对此指标感到满意,因为我们认为没有必要为测试争取100%的代码覆盖率。对于我们来说,达到这样的代码覆盖率水平似乎并不现实。



如何简化新项目的创建?



通常工作在阵营的应用程序的开始是很简单的:ReactDOM.render(<App />, document.getElementById(‘#root’));。但是在需要支持SSR(服务器端渲染)的情况下,此任务将变得更加复杂。另外,如果您的应用程序的依赖项不仅仅包括React,那么您的客户端和服务器代码可能需要使用不同的参数。例如,我们使用react-intl进行国际化,使用react-redux进行全局状态管理,使用react-router进行路由,并使用redux-saga进行异步操作管理。这些依赖项需要进行一些调整。配置这些依赖项的过程可能很复杂。



我们针对此问题的解决方案基于“策略”和“抽象工厂设计模式。我们曾经创建两个不同的类(两个不同的策略):一个用于客户端配置,一个用于服务器配置。这两个类都接收创建的应用程序的参数,包括名称,徽标,缩径词,路由,默认语言,sagas(对于redux-saga),等等。可以从我们的单一存储库的不同包中获取减速器,路线和残渣。然后,此配置用于创建redux存储,sagas中间件,路由器历史记录对象。它还用于加载翻译和呈现应用程序。例如,以下是客户端和服务器策略的签名:



type BootstrapConfiguration = {
  logo: string,
  name: string,
  reducers: ReducersMapObject,
  routes: Route[],
  sagas: Saga[],
};
class AbstractBootstrap {
  configuration: BootstrapConfiguration;
  intl: IntlShape;
  store: Store;
  rootSaga: Task;
abstract public run(): void;
  abstract public render<T>(): T;
  abstract protected createIntl(): IntlShape;
  abstract protected createRootSaga(): Task;
  abstract protected createStore(): Store;
}
//   
class WebBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<ReactNode>(): ReactNode;
}
//   
class ServerBootstrap extends AbstractBootstrap {
  constructor(config: BootstrapConfiguration);
  public render<string>(): string;
}


我们发现这种策略分离很有用,因为根据执行代码的环境,在设置存储,sagas,国际化对象和历史记录方面存在一些差异。例如,使用从服务器预加载的数据并使用redux-devtools-extension创建客户端上的redux存储。服务器上不需要这些。另一个示例是一个国际化对象,该对象在客户端上从navigator.languages获取当前语言,并在服务器上从Accept-Language HTTP标头获取



重要的是要注意,我们很久以前就做出了这个决定。尽管类仍在React应用程序中广泛使用,但没有用于执行应用程序服务器端渲染的简单工具。随着时间的流逝,React库朝着一种功能风格迈出了一步,并且出现了Next.js之类的项目考虑到这一点,如果您正在寻找解决类似问题的方法,我们建议您研究当前的技术。这很有可能使我们找到比我们正在使用的东西更简单,更实用的东西。



如何保持高水平的代码质量?



短绒,测试,类型检查-所有这些都对代码质量产生有益的影响。但是程序员在将代码包含在分支中之前,很容易忘记运行适当的检查master。最好的方法是使这种检查自动运行。有些人喜欢在每次使用Git钩子的提交时执行此操作,直到代码通过所有检查后才允许提交。但是我们认为,使用这种方法,系统会过多地干扰程序员的工作。毕竟,例如,在某个分支上的工作可能要花费几天的时间,而所有这些天将被认为不适合发送到存储库。因此,我们使用持续集成系统检查提交。仅检查与合并请求关联的分支的代码。这使我们避免运行保证不通过的检查,因为当我们确定这些结果能够通过所有检查时,我们通常会要求将工作结果包括在项目的主代码中。



自动代码验证的流程从安装依赖项开始。接下来是类型检查,运行linters,运行单元测试,构建应用程序,运行cypress测试。几乎所有这些任务都是并行执行的。如果这些步骤中的任何一个发生错误,则整个结帐过程将失败,并且相应的分支不能包含在主项目代码中。这是一个工作代码审查系统的示例。





自动代码验证



设置此系统时,我们遇到主要困难是加快检查的执行速度。此任务仍然相关。我们进行了很多优化,现在所有这些检查在大约20分钟内即可稳定下来。也许可以通过并行执行某些柏树测试来改善此指标,但目前它适合我们。



结果



组织大型React应用程序的开发并非易事。为了解决这个问题,程序员需要做出许多决定,需要配置许多工具。同时,对于如何开发此类应用程序的问题,没有一个正确的答案。



到目前为止,我们的系统适合我们。我们希望谈论它会帮助其他面临与我们相同任务的程序员。如果您决定遵循我们的示例,请首先确保此处讨论的内容适合您和您的公司。最重要的是,争取极简主义。不要过于复杂化用于创建它们的应用程序和工具箱。



您将如何处理组织大型React项目开发的任务?






All Articles