作者 | David Gilbertson

译者 | 弯月,责编 | 胡巍巍

出品 | CSDN(ID:CSDNnews)

在设计React应用程序的结构时,理想的结构应该能够把浏览代码的工作量降到最低。

在本文中,我将分享我在设计React应用程序的结构时使用的方法,以及做出每项决定的动机。

在这个过程中我会提到很多我没有使用的方式,因为它们不适合我,但这些方式可能对你有用。

个人的关注点

众所周知,应用程序的结构与计算机无关,也许这一点对你来说显而易见,但我是直到最近脑海中才闪出这个说法。

想象一下,如果应用只用一个文件保存所有的组件、Reducer、Store、工具函数等,会怎么样?

当然,这是一个糟糕的想法。但让我们来想一想为什么这个主意很糟糕。

我敢说你从未认真地思考过这个问题,下面来说一说我的想法吧。问题在于,这个巨型文件根本没法浏览。

但是,如果你在代码的每个区域都添加一个标签,或者是为每个功能添加一个标签呢?而且也许还可以嵌套标签。最后再来一个这些标签的目录怎么样?

这种做法听起来像是胡闹,但我认为至少我们可以确定一件事:在决定文件结构时,你唯一的目标就是最大限度地提高代码的可浏览性。

而你的“文件”只不过是代码中的标签,最终每个文件都会成为大块的JS代码。

这就是为什么你永远无法直接回答这个问题:“哪种方式才能设计出最佳的设计应用程序结构?”这在很大程度上取决于你浏览代码的习惯和偏好,别人无法越俎代庖。

为了找出适合我自己的应用程序结构,我统计了我自己最常见的编程活动:

  • 创建一个新组件。通常我都会复制/粘贴现有的组件。

  • 将一个模块导入另一个模块中。我指的是实际键入这些代码:import { SomeComponent } from '../blah/de/blah.js';

  • 跳转到源代码。我指的是在查看某个文件的时候,其中包含了一个外部引用,比如引用了<HeaderNav>组件,那么我需要跳转到定义该组件的地方。

  • 打开一个已知文件。这一条或许不用多说,但我希望这个列表看起来整齐一些,而此时我想到了“我想打开页首导航”,于是我按下了键盘的快捷键,然后输入文件名打开文件。

  • 浏览一个我不知道名字的文件。也许用户头像下面的下拉菜单组件不是我写的,而且我也不知道它的名字。这时我肯定想浏览这个组件的目录结构。

  • 切换选项卡打开另一个文件。无需多解释。目前我打开了7个文件,我想点击(或使用键盘)切换到另一个选项卡。

接下来,我想了解这些工作的发生频率。我统计了一下去年我创建的组件,每个文件的平均导入数量,然后大概猜测了一下其他人的情况,最后得出了以下结果:

按照我认为的顺序排列

手握这些数据,下面让我们客观地看看设计React应用程序结构的方方面面。首先,逐个介绍一下上述各项。

目录结构

一般的规则是,如果某个模块(工具函数、组件等)仅会在另一个模块中使用,那么我希望将前者嵌套在后者的目录结构中,如下所示:

只有<Header>组件才会引用<HeaderNav>,所以我将后者放在了子目录中。而可以从任何地方引用的<Button>则位于顶层。

这个规则很棒,但我也知道遵循一套超级严格的规则可能很烦人。理论上,所有文件都应该是App和Page的子目录。但我的目录结构可不能搞成那样,我不允许。

听起来可能很草率,但这非常基础。如果你随心所欲创建一个很难浏览的结构,那就是不战而退了。

我认为组件之外的目录结构不是很重要。你可以为是否将Reducer、Action与服务放在同一个目录中而苦恼,直到忧郁成灾。

但是,如果你问我,我会告诉你只需要基本的结构和合理的文件夹名称(ActionCreators、Reducer、Data等)即可。

这可能是第一个我和你的需求和需要不一致的地方。多年以来我养成了一个习惯:很少通过浏览目录结构来打开文件,所以我自然认为目录结构的重要性较低。而且我也从未参与过拥有几百个组件的项目。

然而,如果你喜欢依赖导航目录结构,或者你像Facebook一样有3万多个组件,那么你的需求截然不同。

另外,我建议组件的命名一定要使用全称(而且全局唯一)。例如,HeaderNav位于Header内部,因此你可以争辩它的名字只需叫Nav就可以了。如果这个名字适合你,那没问题。

但是,我喜欢通过键入名称的方式打开文件,并通过选项卡上的文字切换文件。在这两种情况下,完整的名称会非常有帮助性。

而且,如果你遵从边界元素方法(Boundary Element Method,即BEM),块的名称向组件名称看齐,那么肯定需要保证组件名称的全局唯一性。

那容器组件怎么办?

容器组件是一个棘手的组件,因为它们看似是组件,却又不完全是组件。

我有两种方法将容器组件融入结构中:

  • 把它们当成展示型组件处理

  • 放到目录结构之外。让它们生活“在幕后”,只是为组件提供数据

在第一种情况下,实际上你会在标记语言中引用容器组件。以包含页面头部容器组件为例,引用代码如下:

import React from 'react';
import HeaderContainer from './HeaderContainer/HeaderContainer';
import Page from './Page/Page';
import Footer from './Footer/Footer';

const App = (props) => (
  <div>
    <HeaderContainer />

<Page data={props.pageStuff} />

<Footer {...props.propsRelevantToFooter} />
  </div>
);

export default App;

在上述代码中,我向<Page>和<Footer>组件传递了一些特定的数据,然而很明显<HeaderContainer>会照管好自己的数据需求。

如果你采用这种实现方式,那么最后的逻辑结构如下:

第二种方法将容器组件放到了结构之外,你可以把它们看成被打包的组件的实现细节。

所以,可以认为<Header>会把自己打包到容器组件中,然后导出。代码如下:

import React from 'react';
import headerContainer from './headerContainer';

export const Header = () => (
  <header>
    Just header stuff
  </header>
);

export default headerContainer(Header);

然后,引用的时候可以这样写:

import React from 'react';
import Header from './Header/Header';
import Page from './Page/Page';
import Footer from './Footer/Footer';

const App = (props) => (
  <div>
    <Header />

<Page data={props.pageStuff} />

<Footer {...props.propsRelevantToFooter} />
  </div>
);

export default App;

这种方法的弊端在于,很难一下子看出<Header />的数据来自其他地方。但优点在于组件的层次结构少了一层。

如果你喜欢这种方法,那么可以将容器写成一个函数,与为它提供数据的组件放在同一个目录中:

另外请注意,我导出了“原始”的Header,同时还默认导出了容器打包的Header。

前者是为了单元测试,而且你的Linter可能会告诉你,导出的非默认常量与文件同名,这会引发混乱。我比较同意Linter。

我曾在一个中等规模的项目中使用了第一种方法,结果还不错。最近我在一个新项目中(只有6个容器组件)尝试了第二种方法,感觉不太好,所以我会坚持使用第一种方法。

我认为这两种方法都没有问题。

旁注:我认为把控现实是一种艺术,如果你清楚地做出了的抉择,则完全可以理直气壮地说“这并不重要”。

 本身就是容器的组件

我的规则:如果项目中的组件数多于克莱因瓶(Klein bottle)的面数,则我会将每个组件连同其CSS文件和测试文件一起放在一个目录中。

这个规则很常见,但即使你把所有东西都整齐地塞入同一个目录中,仍然有可能犯大错误……

看看你手头那个包含了组件的文件。你可能会在该文件的顶部看到该组件所依赖的一系列导入文件。

除非,这些文件是组件之间共享的CSS类。除此之外,你还有一堆未列出的依赖项。

当然,如果你在<DropDown>组件中加入.modal-wrapper类就可以节省7秒的时间,因为这样做它就会自带你想要的阴影效果,但是,你知道你刚刚给自己的未来挖了多大坑吗?

试图说服别人不要在组件之间共享CSS类,就如同说服人们“避免在JavaScript中使用全局变量”或“给你的鸡打疫苗”,有些人根本不听劝。

CSS-module用户肯定在得意洋洋地摇着大尾巴,自我感觉良好。当然,他们也有充分的理由,因为他们的设置会强制要求显式导入CSS。如果你也喜欢让CSS与组件紧密耦合,那么你也应该使用CSS-module。

文件命名

有一条规则我觉得极其有用:

文件的命名应该与从该文件导出的东西同名。

对于某些人来说,如此明显的规则甚至不值得一提。但我却见过很多代码并没有遵循这条规则,浏览这样的代码极其不爽(请注意,所有这一切都是个人感觉。虽然我说不方便浏览代码,但你完全可以认为“这对我来说不麻烦”,然后认为文件的命名与默认导出相同完全没必要)。

我经常做的事情就是输入文件名,然后打开文件。如果我有一个名为toString的工具函数,那么我十分希望有一个名为toString的文件,然后我只需输入文件名就可以打开了。

我经常做的事情还有一件:通过选项卡切换打开的文件。为此,我希望该选项卡的名称为“toString.js”。

所以,如下结构可能会让我抓狂:

让我不解的是,甚至还有人乐意这么干:

即便你的IDE十分智能,遇到不唯一的文件名会在选项卡名称中显示目录,但仍然会出现大量冗余,而且选项卡很快就显示不下了,这样你仍然无法通过键入文件名打开文件。

无需尝试,我就知道我绝对不喜欢这种方法。就像我与堂兄的新乐队一样:“格格不入”。还是让别人去带你看他们的演出吧。

话虽如此,我知道这背后的缘由:这意味着你的import语句可以写成下面这样:

import Link from '../Link';

而不是这个:

import Link from '../Link/Link';

这明显是一种权衡:缩短import语句,还是导出的文件名?

让我仔细算算……我将模块导入到另一个模块的频率:每周18次。我通过输入文件名打开文件的发生频率:每周840次,而我从选项卡上找到某个名称的频率:每周大约1,892次。

所以,我会在导入路径中加一个额外的单词(依靠自动补齐输入),谢谢。

有些聪明的读者已经对着屏幕大喊了:有两个解决方案可以帮助你的文件名匹配导出的东西,并避免在import语句中输入两次。

第一个解决方案是在每个导出组件的目录中放入一个index.js文件,如下所示:

由于Node在解析导入路径时会查找index.js文件,因此../Link的路径实际上是../Link/index.js,这个文件指向的是实际的组件文件。

如果说在导入时少输几个字符很重要,那么在每个目录中添加一个额外的文件似乎也不错。但我认为这个主意很糟糕,另外再重申一次:纯属个人意见。

第二个“解决方案”大致就是如下这种怪物了吧:

在这种情况下,你知道如果Node没有找到../Link/index.js,那么它就会检查../Link/package.json是否存在。如果存在,就会解析main属性的值。

我认为除非你非常非常讨厌在在import语句中多输一个单词,否则也不至于为每个组件创建一个package.json文件。这种做法真的很奇怪。你往代码中添加的怪物越多,你自己也就越奇葩。

这两种类型的“重定向”文件都意味着你的语句不再指向定义该事物的文件。

以往,这种做法会破坏“跳转到源代码”——这关系到我是否能够轻松愉快地浏览代码。

WebStorm很智能,它能够解决这些跳转的问题(它“明白”我并不想跳转到index.js文件,我想一直跳转到Link.js文件中),但如果你的文本编辑器没有那么智能呢,那么就可能会为你打开很多index.js文件,或者跳转到源代码功能根本不能用。

因此,在采用这种方法之前,请先尝试一番,看看它是否会阻碍你的工作。

.js 与 .jsx扩展

以前,凡是包含JSX的文件我都会使用.jsx扩展,而原始的JavaScript我都会采用.js。这两种扩展名在打开/查看文件时有明显的区别,此外GitHub中还会高亮显示JSX语法。

然而,Facebook建议不要使用.jsx扩展,所以最近我一直在使用.js,我很高兴自己没有浪费太多时间在这个问题上权衡利弊,因为它对我没有任何影响。

我建议在这个问题上,可以扔个硬币来决定。

工具函数的index文件

在撰写本文的时候,我一直在认真思考哪些问题对我来说很重要,实际上我个人对应用结构某个小方面的喜好已经发生了改变。

以前,我习惯为工具函数创建一个index.js文件,如下所示:

这样我就可以像下面这样一次性导入多个工具函数:

import {
  formatDate,
  getAtPath,
  toNumber,
  toString,
} from '../../../../utils';

非常整洁!

无论何时我每添加一个新的工具函数(每周0.8次),只需添加util文件并在index文件中添加一项。

每当我看到某个PR中添加了工具函数,却忘记将它添加到index.js时,我都会提醒开发人员。偶尔我发现有的工具函数不在index.js中,我就会自己动手添加。多么优雅的解决方案。

直到2017年9月,由于某种原因才让我意识到这种做法只会增加复杂性。实际上抛弃index.js,采用如下做法更好:

import formatDate from '../../../../utils/formatDate';
import getAtPath from '../../../../utils/getAtPath';
import toNumber from '../../../../utils/toNumber';
import toString from '../../../../utils/toString';

这种做法可以减少代码行数,减少文件数量,还可以减少向新开发人员解释。

但这些长长的import路径非常刺我的眼,所以下面让我们来看看两个解决方案,分别对应不同的情况。

解决方案之一是使用Webpack的别名解析功能来引用工具函数的目录(不要用相对路径)。

在这里,我将Utils映射成了src/app/utils,最后的结果很漂亮,与我导入其他工具函数的方式非常合拍。

按照惯例,大写“U”可以将工具函数与npm包区分开来

以往,这种解决方案会给有些文本编辑器带来困惑,因为它们不知道Utils/formatDate是什么或在哪里。

但我的IDE很智能,会读取我的Webpack配置(实际上它会运行webpack),就可以正确地找到文件(所以我可以跳转到源代码,还可以利用自动补齐的功能等)。

所以...这是一个漂亮、整洁的解决方案。但其后面是什么呢?

/*  --  webpack.config.shared.js  --  */
export const sharedConfig = {
  alias: {
    'Utils': path.resolve(__dirname, '../src/app/utils/'),
    'Components': path.resolve(__dirname, '../src/app/components/'),
  },
};

/*  --  webpack.config.dev.js  --  */
import { sharedConfig } from './webpack.config.shared.js';

const config = {
  // development config
  resolve: {
    alias: sharedConfig.alias,
  },
};

/*  --  webpack.config.prod.js  --  */
import { sharedConfig } from './webpack.config.shared.js';

const config = {
  // production config
  resolve: {
    alias: sharedConfig.alias,
  },
};

/*  --  SomeComponent.js  --  */
import toNumber from 'Utils/toNumber';
import toString from 'Utils/toString';

虽然这个解决方案很不错,但它有两个负面影响。

  • 增加了复杂性。这样一来,我们就需要更多东西配合使用才能实现完全相同的结果。

  • 降低了清晰度。不熟悉Webpack配置的人看不懂import语句,也不知道它指向什么。

第二种解决方案是说服我自己,不要在意这些细节,花开花落自有时人来人往任由之。

为了说服自己,我想到了我经常输入的一条导入路径,结果却发现我输入这条路径的频率与我煮咖啡一样高。

于是,我告诉自己,其实呢,输入8个点和5个斜杠也没有那么难啊,至少没有煮咖啡那么难:将咖啡豆放到咖啡机里,从糖罐子里称一茶匙的糖放入杯子中,然后再去奶牛那里挤一点点牛奶,然后再按下咖啡机上杯子的图标。

这两种解决方案的权衡代表了许多不同的决定(生活方式与编程方式),因此也许我可以利用这个机会演绎一番清晰度/模糊性与简单性/复杂性矩阵。

简称ClObSiCo

对我来说,这两者之间非常接近。最后,我决定尽可能保持清晰和简洁,所以即使import语句中一连串的../../../../很刺眼,但它依然赢了。

组件的index文件

这不是我的菜,但为了坚持与import语句中大量的点作斗争,我还有最后一招:为你的组件创建一个库。

也许你会对此感兴趣:

import React from 'react';
import {
  Button,
  Footer,
  Header,
  Page,
} from 'Components';

你已经知道如何在Webpack配置中执行此操作了吧:

const config = {
  // other stuff
  resolve: {
    alias: {
      'Components': path.resolve(__dirname, '../src/app/components/'),
    },
  },
};

接下来,在组件目录中添加一个index.js文件,每个组件一行,如下所示:

鼓掌!

每个文件有多个导出

在大多数情况下,每个文件只有一个导出——导出与文件名相同,我认为这是非常适用于组件和实用程序函数的一个很好的通用规则。

但我认为这不适用于常量。起初我也很喜欢在一个文件中编写所有的action,直到我发现这是一种负担。

reducer亦是如此。根据我的经验,在一个文件中编写8个10行代码的reducer,还是创建8个文件,二者并没有多大区别。

如果你觉得这对你找到特定代码的速度有很大影响,那么就选择适合你的方法。如果Redux才是你的真命天子,那么就选它好了,无所谓。

 团队的关注点

接下来让我们紧扣本文的主题。如果你正在独自开发一个项目,那么你可以找到万无一失的React结构。事实上,我认为这非常值得。

但是,团队的人数越多,你会发现“最优”的可能性就越小,其他因素就越有发挥的空间。

最重要的是妥协。注意区分偏见。通过以上内容,你可以说对于某些项目来说,如果团队成员表达出强烈的偏好,那么我肯定乐意采取另一种方式。

如果有人真的想使用.jsx扩展名,或者使用Utils别名,那我也不会有意见,因为虽然这不是我的偏好,但它不会降低我的工作效率。

但如果有人真的特别特别希望每个文件都命名为index.js,那就是搞事情啊。

还有一个因素需要考虑:如果团队中有30个开发,而且你正在启动一个新项目,那么你可能希望尽可能地选用之前项目的结构,因为这样就不需要重复很多基础工作了。

或许你想从过去的错误中吸取教训,然后建立不同的结构,修复过往的那些失误。

还有一件小事:随着团队一天天壮大,终有一天git冲突会愈演愈烈,兴许届时小文件就反败为胜了。

如果团队中的开发人员水平层次不齐,那么你应该大力支持简单性和清晰度。

另一方面,如果你有一支经验丰富的前端工程师团队,那就彻底放飞自我吧,想搞得多复杂都行。只要每个人都跟得上节奏,无论外观看起来多么奇葩都不重要。

 

总结

我坦白,我不擅长写总结。我就在想,我刚写了一篇博文给你看,你还要我怎样?!

所以本段不是总结,但我认为在所有关系到应用程序结构的方法中,最关键的方面还是人们处理分歧的方式。

网上大量的评论总结起来就一句话:“我不同意,我很愤怒。”

遗憾的是,当两个理性的人持不同意见时,往往就会发生一些有趣的事情,就让我们安安静静地做一名吃瓜群众吧。

既然收尾工作已经一塌糊涂了,那么最后给你推荐一部电影吧,怎么样?如果你喜欢《第九区》,但还没看《超能查派》,那就抓紧去看吧。

原文:https://medium.com/hackernoon/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed

本文为CSDN翻译,转载请注明来源出处。

【End】

Facebook工程师给Python学习者的进阶指南

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

 热 文 推 荐 

☞@程序员,一文掌握 Web 应用中的图片优化技巧!

☞Google 搜索点击量不到 50%?

☞马云马斯克激辩:AI 是威胁还是被低估了?

程序员易踩的 9 大坑!

☞99年少年12岁时买下100枚比特币, 如今却将所有积蓄压在一个不知名的代币上,还放话将超越Libra!

强推!阿里数据科学家一次讲透数据中台

☞冠军奖3万元!CSDN×易观算法大赛开赛啦

可惜了,你们只看到“双马会”大型尬聊

☞如何写出让同事无法维护的代码?

点击阅读原文,输入关键词,即可搜索您想要的CSDN文章。

你点的每个“在看”,我都认真当成了喜欢

这才是设计 React 的万金油!相关推荐

  1. 如何设计 React 代码结构?

    怎样设计一个项目的文件和组件的结构.甚至是某个组件的内部结构?这个问题永远没有正确答案. 作者 | Mathilde Wærstad 译者 | 弯月,责编 | 郭芮 出品 | CSDN(ID:CSDN ...

  2. 如何优雅的设计 React 组件

    为什么80%的码农都做不了架构师?>>>    作者:晓冬 本文原创,转载请注明作者及出处 如今的 Web 前端已被 React.Vue 和 Angular 三分天下,一统江山十几年 ...

  3. 收集灵感必备|文字这样组合排版那才叫设计

    文字是海报组成的一个重要元素,占有很大分量,然而文字的组合排版就直接影响到视觉传达的效果. 所以文字排版在一定程度上还具有吸引人们视觉的功能, 集设给大家收集海量优秀文字的组合排版合集,希望能够帮到大 ...

  4. React 设计思想

    React 设计思想 译者序:本文是 React 核心开发者.有 React API 终结者之称的 Sebastian Markbåge 撰写,阐述了他设计 React 的初衷.阅读此文,你能站在更高 ...

  5. 源码解析 React Hook 构建过程

    2018 年的 React Conf 上 Dan Abramov 正式对外介绍了React Hook,这是一种让函数组件支持状态和其他 React 特性的全新方式,并被官方解读为这是下一个 5 年 R ...

  6. 回归架构本真:从规划、思维到设计,构建坚不可摧的架构根基

    关于什么是架构,业界从来没有一个统一的定义.Martin Fowler在<企业应用架构模式>中也没有对其给出定义,只是提到能够统一的内容有两点: 最高层次的系统分解: 系统中不易改变的决定 ...

  7. ajax mysql项目 react_React16时代,该用什么姿势写 React ?

    React16 后的各功能点是多个版本陆陆续续迭代增加的,本篇文章的讲解是建立在 16.6.0 版本上 本篇文章主旨在介绍 React16 之后版本中新增或修改的地方,所以对于 React16 之前版 ...

  8. pyqt 获取 UI 中组件_你想知道的React组件设计模式这里都有(上)

    本文梳理了容器与展示组件.高阶组件.render props这三类React组件设计模式 往期回顾:HBaseCon Asia 2019 Track 3 概要回顾 随着 React 的发展,各种组件设 ...

  9. 群里分享的react的收藏一下!今日周末,改了个表单验证然后无所事事了!

    今日周末,改了个表单验证然后无所事事了,然后把昨天群里分享的react的收藏一下尽管现在还在研究angular和nodeJs毕竟刚刚开始用有点不熟...没准以后会研究一下react毕竟看着下面这张图还 ...

最新文章

  1. python3 hmac算法简介
  2. 机器学习知识点(十六)集成学习AdaBoost算法Java实现
  3. 如何构建高扩展性网站?
  4. navicat使用触发器
  5. SAP Hybris Commerce里的数据库表
  6. remmina连接xfce桌面的centos7
  7. Hibernate5-一对多双向关联-迫切左外连接-HQL
  8. 移动开发痛点之一-接口验证之PostMan图文教程
  9. Android之Camera拍照
  10. 大年三十问候导师的后果
  11. 错误的太极观念造成膝盖损伤
  12. 计算机网络专业以后装网线,一种便于安装的计算机网络用网线安装盒的制作方法...
  13. 基于matlab的齿轮,基于matlab的故障齿轮分析.doc
  14. GB28181设备接入实现web无插件多屏直播
  15. 一次小米路由器3刷机的翻车记录
  16. Ubuntu下载功能包时出现:检验数字签名时出错,此仓库未被更新,所以仍然使用此前的索引文件的解决办法
  17. Java开发人员幽默外号,姓李的幽默外号 - 经典语录大全
  18. 使用域名访问远程jupyter_如何设置远程访问的Jupyter Notebook服务器-01(之预备知识:什么是端口号?)...
  19. mysql jdbc batch_JDBC批处理(batch)
  20. Spark读HBASE - shc方案

热门文章

  1. UVA 10596 Morning Walk
  2. 《Linux编程》作业 ·003【文件I/O操作】
  3. linux环境下编译Qt源码
  4. [论文阅读] Shallow Attention Network for Polyp Segmentation
  5. [DFS|剪枝] leetcode 22 括号生成
  6. centos查看文件修改历史_Linux环境下查看历史操作命令及清除方法
  7. 前端问题求助input type=“range”问题求助
  8. 剑指Offer之寻找二叉树下一个节点
  9. linux数据,Linux数据
  10. 记录——《C Primer Plus (第五版)》第九章编程练习第六题