这才是设计 React 的万金油!

Posted CSDN

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这才是设计 React 的万金油!相关的知识,希望对你有一定的参考价值。

这才是设计 React 的万金油!
作者 | David Gilbertson
译者 | 弯月,责编 | 胡巍巍
出品 | CSDN(ID:CSDNnews)
在设计React应用程序的结构时,理想的结构应该能够把浏览代码的工作量降到最低。 
在本文中,我将分享我在设计React应用程序的结构时使用的方法,以及做出每项决定的动机。
在这个过程中我会提到很多我没有使用的方式,因为它们不适合我,但这些方式可能对你有用。 


这才是设计 React 的万金油!
个人的关注点
 
众所周知,应用程序的结构与计算机无关,也许这一点对你来说显而易见,但我是直到最近脑海中才闪出这个说法。 
想象一下,如果应用只用一个文件保存所有的组件、Reducer、Store、工具函数等,会怎么样? 
当然,这是一个糟糕的想法。但让我们来想一想为什么这个主意很糟糕。 
我敢说你从未认真地思考过这个问题,下面来说一说我的想法吧。问题在于,这个巨型文件根本没法浏览。
但是,如果你在代码的每个区域都添加一个标签,或者是为每个功能添加一个标签呢?而且也许还可以嵌套标签。最后再来一个这些标签的目录怎么样? 
这种做法听起来像是胡闹,但我认为至少我们可以确定一件事:在决定文件结构时,你唯一的目标就是最大限度地提高代码的可浏览性。
而你的“文件”只不过是代码中的标签,最终每个文件都会成为大块的JS代码。 
这就是为什么你永远无法直接回答这个问题:“哪种方式才能设计出最佳的设计应用程序结构?”这在很大程度上取决于你浏览代码的习惯和偏好,别人无法越俎代庖。 
为了找出适合我自己的应用程序结构,我统计了我自己最常见的编程活动:
  • 创建一个新组件。通常我都会复制/粘贴现有的组件。

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

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

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

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

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

 接下来,我想了解这些工作的发生频率。我统计了一下去年我创建的组件,每个文件的平均导入数量,然后大概猜测了一下其他人的情况,最后得出了以下结果: 
这才是设计 React 的万金油!
按照我认为的顺序排列 
手握这些数据,下面让我们客观地看看设计React应用程序结构的方方面面。首先,逐个介绍一下上述各项。

这才是设计 React 的万金油!
目录结构


一般的规则是,如果某个模块(工具函数、组件等)仅会在另一个模块中使用,那么我希望将前者嵌套在后者的目录结构中,如下所示: 
这才是设计 React 的万金油!
只有<Header>组件才会引用<HeaderNav>,所以我将后者放在了子目录中。而可以从任何地方引用的<Button>则位于顶层。 
这个规则很棒,但我也知道遵循一套超级严格的规则可能很烦人。理论上,所有文件都应该是App和Page的子目录。但我的目录结构可不能搞成那样,我不允许。 
听起来可能很草率,但这非常基础。如果你随心所欲创建一个很难浏览的结构,那就是不战而退了。 
我认为组件之外的目录结构不是很重要。你可以为是否将Reducer、Action与服务放在同一个目录中而苦恼,直到忧郁成灾。
但是,如果你问我,我会告诉你只需要基本的结构和合理的文件夹名称(ActionCreators、Reducer、Data等)即可。 
这可能是第一个我和你的需求和需要不一致的地方。多年以来我养成了一个习惯:很少通过浏览目录结构来打开文件,所以我自然认为目录结构的重要性较低。而且我也从未参与过拥有几百个组件的项目。 
然而,如果你喜欢依赖导航目录结构,或者你像Facebook一样有3万多个组件,那么你的需求截然不同。 
另外,我建议组件的命名一定要使用全称(而且全局唯一)。例如,HeaderNav位于Header内部,因此你可以争辩它的名字只需叫Nav就可以了。如果这个名字适合你,那没问题。
但是,我喜欢通过键入名称的方式打开文件,并通过选项卡上的文字切换文件。在这两种情况下,完整的名称会非常有帮助性。 
而且,如果你遵从边界元素方法(Boundary Element Method,即BEM),块的名称向组件名称看齐,那么肯定需要保证组件名称的全局唯一性。

这才是设计 React 的万金油!
那容器组件怎么办?

容器组件是一个棘手的组件,因为它们看似是组件,却又不完全是组件。 
我有两种方法将容器组件融入结构中:
  • 把它们当成展示型组件处理

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

在第一种情况下,实际上你会在标记语言中引用容器组件。以包含页面头部容器组件为例,引用代码如下: 
  
    
    
  
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>会照管好自己的数据需求。 
如果你采用这种实现方式,那么最后的逻辑结构如下:
这才是设计 React 的万金油!
第二种方法将容器组件放到了结构之外,你可以把它们看成被打包的组件的实现细节。 
所以,可以认为<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 />的数据来自其他地方。但优点在于组件的层次结构少了一层。 
如果你喜欢这种方法,那么可以将容器写成一个函数,与为它提供数据的组件放在同一个目录中: 
这才是设计 React 的万金油!
另外请注意,我导出了“原始”的Header,同时还默认导出了容器打包的Header。
前者是为了单元测试,而且你的Linter可能会告诉你,导出的非默认常量与文件同名,这会引发混乱。我比较同意Linter。
我曾在一个中等规模的项目中使用了第一种方法,结果还不错。最近我在一个新项目中(只有6个容器组件)尝试了第二种方法,感觉不太好,所以我会坚持使用第一种方法。 
我认为这两种方法都没有问题。 
旁注:我认为把控现实是一种艺术,如果你清楚地做出了的抉择,则完全可以理直气壮地说“这并不重要”。


这才是设计 React 的万金油!
 本身就是容器的组件

我的规则:如果项目中的组件数多于克莱因瓶(Klein bottle)的面数,则我会将每个组件连同其CSS文件和测试文件一起放在一个目录中。
这个规则很常见,但即使你把所有东西都整齐地塞入同一个目录中,仍然有可能犯大错误…… 
看看你手头那个包含了组件的文件。你可能会在该文件的顶部看到该组件所依赖的一系列导入文件。 
除非,这些文件是组件之间共享的CSS类。除此之外,你还有一堆未列出的依赖项。 
当然,如果你在<DropDown>组件中加入.modal-wrapper类就可以节省7秒的时间,因为这样做它就会自带你想要的阴影效果,但是,你知道你刚刚给自己的未来挖了多大坑吗? 
试图说服别人不要在组件之间共享CSS类,就如同说服人们“避免在javascript中使用全局变量”或“给你的鸡打疫苗”,有些人根本不听劝。  
CSS-module用户肯定在得意洋洋地摇着大尾巴,自我感觉良好。当然,他们也有充分的理由,因为他们的设置会强制要求显式导入CSS。如果你也喜欢让CSS与组件紧密耦合,那么你也应该使用CSS-module。

这才是设计 React 的万金油!
文件命名


有一条规则我觉得极其有用: 
文件的命名应该与从该文件导出的东西同名。 
对于某些人来说,如此明显的规则甚至不值得一提。但我却见过很多代码并没有遵循这条规则,浏览这样的代码极其不爽 (请注意,所有这一切都是个人感觉。 虽然我说不方便浏览代码,但你完全可以认为“这对我来说不麻烦”,然后认为文件的命名与默认导出相同完全没必要 )。
我经常做的事情就是输入文件名,然后打开文件。如果我有一个名为toString的工具函数,那么我十分希望有一个名为toString的文件,然后我只需输入文件名就可以打开了。
我经常做的事情还有一件:通过选项卡切换打开的文件。为此,我希望该选项卡的名称为“toString.js”。 
所以,如下结构可能会让我抓狂:  
这才是设计 React 的万金油!
让我不解的是,甚至还有人乐意这么干: 
这才是设计 React 的万金油!
即便你的IDE十分智能,遇到不唯一的文件名会在选项卡名称中显示目录,但仍然会出现大量冗余,而且选项卡很快就显示不下了,这样你仍然无法通过键入文件名打开文件。
无需尝试,我就知道我绝对不喜欢这种方法。就像我与堂兄的新乐队一样:“格格不入”。还是让别人去带你看他们的演出吧。
话虽如此,我知道这背后的缘由:这意味着你的import语句可以写成下面这样: 
  
    
    
  
import Link  from  '../Link';
而不是这个: 
  
    
    
  
import Link  from  '../Link/Link';
这明显是一种权衡:缩短import语句,还是导出的文件名? 
让我仔细算算……我将模块导入到另一个模块的频率:每周18次。我通过输入文件名打开文件的发生频率:每周840次,而我从选项卡上找到某个名称的频率:每周大约1,892次。 
所以,我会在导入路径中加一个额外的单词(依靠自动补齐输入),谢谢。 
有些聪明的读者已经对着屏幕大喊了:有两个解决方案可以帮助你的文件名匹配导出的东西,并避免在import语句中输入两次。 
第一个解决方案是在每个导出组件的目录中放入一个index.js文件,如下所示:
这才是设计 React 的万金油!
由于Node在解析导入路径时会查找index.js文件,因此../Link的路径实际上是../Link/index.js,这个文件指向的是实际的组件文件。 
如果说在导入时少输几个字符很重要,那么在每个目录中添加一个额外的文件似乎也不错。但我认为这个主意很糟糕,另外再重申一次:纯属个人意见。 
第二个“解决方案”大致就是如下这种怪物了吧: 
这才是设计 React 的万金油!
在这种情况下,你知道如果Node没有找到../Link/index.js,那么它就会检查../Link/package.json是否存在。如果存在,就会解析main属性的值。 
我认为除非你非常非常讨厌在在import语句中多输一个单词,否则也不至于为每个组件创建一个package.json文件。这种做法真的很奇怪。你往代码中添加的怪物越多,你自己也就越奇葩。 
这两种类型的“重定向”文件都意味着你的语句不再指向定义该事物的文件。 
以往,这种做法会破坏“跳转到源代码”——这关系到我是否能够轻松愉快地浏览代码。
WebStorm很智能,它能够解决这些跳转的问题(它“明白”我并不想跳转到index.js文件,我想一直跳转到Link.js文件中),但如果你的文本编辑器没有那么智能呢,那么就可能会为你打开很多index.js文件,或者跳转到源代码功能根本不能用。 
因此,在采用这种方法之前,请先尝试一番,看看它是否会阻碍你的工作。

这才是设计 React 的万金油!
.js 与 .jsx扩展

以前,凡是包含JSX的文件我都会使用.jsx扩展,而原始的JavaScript我都会采用.js。这两种扩展名在打开/查看文件时有明显的区别,此外GitHub中还会高亮显示JSX语法。 
然而,Facebook建议不要使用.jsx扩展,所以最近我一直在使用.js,我很高兴自己没有浪费太多时间在这个问题上权衡利弊,因为它对我没有任何影响。 
我建议在这个问题上,可以扔个硬币来决定。

这才是设计 React 的万金油!
工具函数的index文件
 
在撰写本文的时候,我一直在认真思考哪些问题对我来说很重要,实际上我个人对应用结构某个小方面的喜好已经发生了改变。 
以前,我习惯为工具函数创建一个index.js文件,如下所示: 
这才是设计 React 的万金油!
这样我就可以像下面这样一次性导入多个工具函数: 
  
    
    
  
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,最后的结果很漂亮,与我导入其他工具函数的方式非常合拍。 
这才是设计 React 的万金油!
按照惯例,大写“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个斜杠也没有那么难啊,至少没有煮咖啡那么难:将咖啡豆放到咖啡机里,从糖罐子里称一茶匙的糖放入杯子中,然后再去奶牛那里挤一点点牛奶,然后再按下咖啡机上杯子的图标。 
这两种解决方案的权衡代表了许多不同的决定(生活方式与编程方式),因此也许我可以利用这个机会演绎一番清晰度/模糊性与简单性/复杂性矩阵。 
这才是设计 React 的万金油!
简称ClObSiCo 
对我来说,这两者之间非常接近。最后,我决定尽可能保持清晰和简洁,所以即使import语句中一连串的../../../../很刺眼,但它依然赢了。

这才是设计 React 的万金油!
组件的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文件,每个组件一行,如下所示: 

这才是设计 React 的万金油!
鼓掌!

这才是设计 React 的万金油!
每个文件有多个导出

在大多数情况下,每个文件只有一个导出——导出与文件名相同,我认为这是非常适用于组件和实用程序函数的一个很好的通用规则。 
但我认为这不适用于常量。起初我也很喜欢在一个文件中编写所有的action,直到我发现这是一种负担。
reducer亦是如此。根据我的经验,在一个文件中编写8个10行代码的reducer,还是创建8个文件,二者并没有多大区别。 
如果你觉得这对你找到特定代码的速度有很大影响,那么就选择适合你的方法。如果Redux才是你的真命天子,那么就选它好了,无所谓。

这才是设计 React 的万金油!
 团队的关注点

接下来让我们紧扣本文的主题。如果你正在独自开发一个项目,那么你可以找到万无一失的React结构。事实上,我认为这非常值得。 
但是,团队的人数越多,你会发现“最优”的可能性就越小,其他因素就越有发挥的空间。 
最重要的是妥协。注意区分偏见。通过以上内容,你可以说对于某些项目来说,如果团队成员表达出强烈的偏好,那么我肯定乐意采取另一种方式。
如果有人真的想使用.jsx扩展名,或者使用Utils别名,那我也不会有意见,因为虽然这不是我的偏好,但它不会降低我的工作效率。
但如果有人真的特别特别希望每个文件都命名为index.js,那就是搞事情啊。 
还有一个因素需要考虑:如果团队中有30个开发,而且你正在启动一个新项目,那么你可能希望尽可能地选用之前项目的结构,因为这样就不需要重复很多基础工作了。
或许你想从过去的错误中吸取教训,然后建立不同的结构,修复过往的那些失误。 
还有一件小事:随着团队一天天壮大,终有一天git冲突会愈演愈烈,兴许届时小文件就反败为胜了。 
如果团队中的开发人员水平层次不齐,那么你应该大力支持简单性和清晰度。
另一方面,如果你有一支经验丰富的前端工程师团队,那就彻底放飞自我吧,想搞得多复杂都行。只要每个人都跟得上节奏,无论外观看起来多么奇葩都不重要。

这才是设计 React 的万金油! 
总结

我坦白,我不擅长写总结。我就在想,我刚写了一篇博文给你看,你还要我怎样?! 
所以本段不是总结,但我认为在所有关系到应用程序结构的方法中,最关键的方面还是人们处理分歧的方式。
网上大量的评论总结起来就一句话:“我不同意,我很愤怒。”
遗憾的是,当两个理性的人持不同意见时,往往就会发生一些有趣的事情,就让我们安安静静地做一名吃瓜群众吧。 
既然收尾工作已经一塌糊涂了,那么最后给你推荐一部电影吧,怎么样?如果你喜欢《第九区》,但还没看《超能查派》,那就抓紧去看吧。
原文:https://medium.com/hackernoon/the-100-correct-way-to-structure-a-react-app-or-why-theres-no-such-thing-3ede534ef1ed
本文为CSDN翻译,转载请注明来源出处。
【End】
这才是设计 React 的万金油!
 热 文 推 荐 
Google 搜索点击量不到 50%?
点击阅读原文,输入关键词,即可搜索您想要的CSDN文章。
你点的每个“在看”,我都认真当成了喜欢

以上是关于这才是设计 React 的万金油!的主要内容,如果未能解决你的问题,请参考以下文章

图解电商支付架构设计,这才是真电商!

图解电商支付架构设计,这才是真电商!

别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!

我去,这才是完美的单例模式!

别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式。。

别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!