这才是设计 React 的万金油!
Posted CSDN
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了这才是设计 React 的万金油!相关的知识,希望对你有一定的参考价值。
在设计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文件,或者跳转到源代码功能根本不能用。
因此,在采用这种方法之前,请先尝试一番,看看它是否会阻碍你的工作。
以前,凡是包含JSX的文件我都会使用.jsx扩展,而原始的JavaScript我都会采用.js。这两种扩展名在打开/查看文件时有明显的区别,此外GitHub中还会高亮显示JSX语法。
然而,Facebook建议不要使用.jsx扩展,所以最近我一直在使用.js,我很高兴自己没有浪费太多时间在这个问题上权衡利弊,因为它对我没有任何影响。
在撰写本文的时候,我一直在认真思考哪些问题对我来说很重要,实际上我个人对应用结构某个小方面的喜好已经发生了改变。
以前,我习惯为工具函数创建一个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';
第二种解决方案是说服我自己,不要在意这些细节,花开花落自有时人来人往任由之。
为了说服自己,我想到了我经常输入的一条导入路径,结果却发现我输入这条路径的频率与我煮咖啡一样高。
于是,我告诉自己,其实呢,输入8个点和5个斜杠也没有那么难啊,至少没有煮咖啡那么难:将咖啡豆放到咖啡机里,从糖罐子里称一茶匙的糖放入杯子中,然后再去奶牛那里挤一点点牛奶,然后再按下咖啡机上杯子的图标。
这两种解决方案的权衡代表了许多不同的决定(生活方式与编程方式),因此也许我可以利用这个机会演绎一番清晰度/模糊性与简单性/复杂性矩阵。
对我来说,这两者之间非常接近。最后,我决定尽可能保持清晰和简洁,所以即使import语句中一连串的../../../../很刺眼,但它依然赢了。
这不是我的菜,但为了坚持与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文章。
以上是关于这才是设计 React 的万金油!的主要内容,如果未能解决你的问题,请参考以下文章
图解电商支付架构设计,这才是真电商!
图解电商支付架构设计,这才是真电商!
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
我去,这才是完美的单例模式!
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式。。
别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!