这才是设计 React 的万金油

这才是设计 React 的万金油

首页休闲益智我是键盘侠无限咖啡豆更新时间:2024-06-15

作者 | David Gilbertson

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

出品 | CSDN(ID:CSDNnews)

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

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

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

个人的关注

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

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

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

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

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

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

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

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

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

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

按照我认为的顺序排列

手握这些数据,下面让我们客观地看看设计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';

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

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

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

于是,我告诉自己,其实呢,输入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】

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved