在日常开发中,团队中每个人组织代码的方式不尽相同。下面我们就从代码结构的角度来看看如何组织一个更加优雅的 React 组件!
我们通常会在组件文件顶部导入组件所需的依赖项。对于不同类别的依赖项,建议对它们进行分组,这有助于帮助我们更好的理解组件。可以将导入的依赖分为四类:
//?外部依赖
import?React?from?"react";
import?{?useRouter?}?from?"next/router";
//?内部依赖
import?{?Button?}?from?"../src/components/button";
//?本地依赖
import?{?Tag?}?from?"./tag";
import?{?Subscribe?}?from?"./subscribe";
//?样式
import?styles?from?"./article.module.scss";
外部依赖:?外部依赖主要是第三方依赖,这些依赖定义在package.json
文件中并从node_modules
?中导入;
内部依赖:?内部依赖主要是位于组件文件夹之外的可重用的组件或模块,这些导入都应该使用相对导入语法,以?../
?开头。通常,大部分导入的依赖项都属于这一类。因此,如果需要的话,我们可以将这一类组件进一步分离,例如:UI组件、数据相关的导入、services
等;
本地依赖:?本地依赖主要是与组件位于同一文件夹中的依赖项或子组件。这些依赖项的所有导入路径应以./
开头。主要是比较大的组件会包含本地依赖项;
样式:?最后这一部分大多数情况下只包含一个导入——样式文件。如果导入了多个样式表,就需要考虑样式的拆分是否有问题。
对导入依赖项进行手动分组可能比较麻烦,Prettier
?可以帮助我们自动格式化代码。可以使用?prettier-plugin-sort-imports
?插件来自动格式化依赖项导入。需要在项目根目录创建prettier.config.js
配置文件,并在里面配置规则:
module.exports?=?{
??//?其他?Prettier?配置
??importOrder:?[
????//?默认情况下,首先会放置外部依赖项
????//?内部依赖
????"^../(.*)",
????//?本地依赖项,样式除外
????"^./((?!scss).)*$",
????//?其他
????"^./(.*)",
??],
??importOrderSeparation:?true,
};
下面是该插件官方给出的例子,输入如下:
import?React,?{
????FC,
????useEffect,
????useRef,
????ChangeEvent,
????KeyboardEvent,
}?from?'react';
import?{?logger?}?from?'@core/logger';
import?{?reduce,?debounce?}?from?'lodash';
import?{?Message?}?from?'../Message';
import?{?createServer?}?from?'@server/node';
import?{?Alert?}?from?'@ui/Alert';
import?{?repeat,?filter,?add?}?from?'../utils';
import?{?initializeApp?}?from?'@core/app';
import?{?Popup?}?from?'@ui/Popup';
import?{?createConnection?}?from?'@server/database';
格式化之后的输出如下:
import?{?debounce,?reduce?}?from?'lodash';
import?React,?{
????ChangeEvent,
????FC,
????KeyboardEvent,
????useEffect,
????useRef,
}?from?'react';
import?{?createConnection?}?from?'@server/database';
import?{?createServer?}?from?'@server/node';
import?{?initializeApp?}?from?'@core/app';
import?{?logger?}?from?'@core/logger';
import?{?Alert?}?from?'@ui/Alert';
import?{?Popup?}?from?'@ui/Popup';
import?{?Message?}?from?'../Message';
import?{?add,?filter,?repeat?}?from?'../utils';
prettier-plugin-sort-imports
:https://github.com/trivago/prettier-plugin-sort-imports
在导入依赖项的下方,通常会放那些使用 TypeScript 或 Flow 等静态类型检查器定义的文件级常量和类型定义。
组件中的所有?magic
?值,例如字符串或者数字,都应该放在文件的顶部,导入依赖项的下方。由于这些都是静态常量,这意味着它们的值不会改变。因此将它们放在组件中是没有意义的,因为放在组件中的话,它们会在每次重新渲染组件时重新创建。
const?MAX_READING_TIME?=?10;
const?META_TITLE?=?"Hello?World";
对于更复杂的静态数据结构,可以将其提取到一个单独的文件中,以保持组件代码整洁。
下面是使用 TypeScript 声明的组件?props
?的类型:
interface?Props?{
??id:?number;
??name:?string;
??title:?string;
??meta:?Metadata;
}
如果这个?props
?的类型不需要导出,可以使用?Props
?作为接口名称,这样可以帮助我们立即识别组件?props
?的类型定义,并将其与其他类型区分开。
只有当这个?Props
?类型需要在多个组件中使用时,才需要添加组件名称,例如ButtonProps
,因为它在导入另一个组件时,不应该与另一个组件的Props
类型冲突。
定义函数组件的方式有两种:函数声明和箭头函数,?推荐使用函数声明的形式,因为这就是语法声明的内容:函数。官方文档的示例中也使用了这种方法:
function?Article(props:?Props)?{
??/**/
}
只会在必须使用?forwardRef
?时才使用箭头函数:
const?Article?=?React.forwardRef<HTMLArticleElement,?Props>(
??(props,?ref)?=>?{
????/**/
??}
);
通常会在组件最后默认导出组件:
export?default?Article;
接下来,我们就需要在组件里面进行变量的声明。注意,即使使用?const
?声明,这里也称为变量,因为它们的值通常会在不同的渲染之间发生变化,只有在执行单个渲染过程时是恒定的。
const?{?id,?name,?title?}?=?props;
const?router?=?useRouter();
const?initials?=?getInitials(name);
这里通常包含在组件级别使用的所有变量,使用?const
?或?let
?定义,具体取决于它们在渲染期间是否更改其值:
解构数据:通常来自?props
、数据?stores
?或组件的?state
;
Hooks:自定义hooks、框架内置 Hooks,例如?useState
、useReducer
、useRef
、useCallback
?或?useMemo
;
在整个组件中使用的已处理数据,由函数计算得出;
一些较大的组件可能需要在组件中声明很多变量。这种情况下,建议根据它们的初始化方法或者用途对它们进行分组:
//?框架?hooks
const?router?=?useRouter();
//?自定义?hooks
const?user?=?useLoggedUser();
const?theme?=?useTheme();
//?从?props?中解构的数据
const?{?id,?title,?meta,?content,?onSubscribe,?tags?}?=?props;
const?{?image,?author,?date?}?=?meta;
//?组件状态
const?[email,?setEmail]?=?React.useState("");
const?[showMenu,?toggleMenu]?=?React.useState(false);
const?[activeTag,?dispatch]?=?React.useReducer(reducer,?tags);
//?记忆数据
const?subscribe?=?React.useCallback(onSubscribe,?[id]);
const?summary?=?React.useMemo(()?=>?getSummary(content),?[content]);
//?refs
const?sideMenuRef?=?useRef<HTMLDivElement>(null);
const?subscribeRef?=?useRef<HTMLButtonElement>(null);
//?计算数据
const?initials?=?getInitials(author);
const?formattedDate?=?getDate(date);
变量分组的方法在不同组件之间可能会存在很大的差异,它取决于变量的数量和类型。关键是要将相关变量放在一起,在不同组之间添加一个空行来提高代码的可读性。
注:上面代码中的注释仅用于标注分组类型,在实际项目中不会写这些注释。
Effects 部分通常会写在变量声明之后,它们可能是React中最复杂的构造,但从语法的角度来看它们非常简单:
useEffect(()?=>?{
??setLogo(theme?===?"dark"???"white"?:?"black");
},?[theme]);
任何包含在effect
之内但是在其外部定义的变量,都应该包含在依赖项的数组中。
除此之外,还应该使用return
来清理副作用:
useEffect(()?=>?{
??function?onScroll()?{
????/*...*/
??}
??window.addEventListener("scroll",?onScroll);
??return?()?=>?window.removeEventListener("scroll",?onScroll);
},?[]);
组件的核心就是它的内容,React 组件的内容使用 JSX 语法定义并在浏览器中呈现为 HTML。所以,推荐将函数组件的?return
?语句尽可能靠近文件的顶部。其他一切都只是细节,它们应该放在文件较下的位置。
function?Article(props:?Props)?{
??//?变量声明
??//?effects
??//???自定义的函数不建议放在?return?部分的前面
??function?getInitials()?{
????/*...*/
??}
??return?/*?content?*/;
}
export?default?Article;
function?Article(props:?Props)?{
??//?变量声明
??//?effects
??return?/*?content?*/;
??//???自定义的函数建议放在?return?部分的后面
??function?getInitials()?{
????/*...*/
??}
}
export?default?Article;
难道return
不应该放在函数的最后吗?其实不然,对于常规函数,肯定是要将return
放在最后的。然而,React组件并不是简单的函数,它们通常包含具有各种用途的嵌套函数,例如事件处理程序。最后的return
语句以及前面的一堆其他函数,实际上阻碍了代码的阅读,使得很难找到组件渲染的内容:
很难搜索该return
语句,因为可能有来自其他嵌套函数的多个?return
?语句;
在文件末尾滚动查找?return
?语句并不能很容易找到它,因为返回的 JSX 块可能非常大。
当然,可以根据个人喜好来决定函数定义的位置。如果将函数放在return
的下方,那么如果想要使用箭头函数来自定义函数,那就只能使用var
来定义,因为let
和const
不存在变量提升,不能在定义的箭头函数之前使用它。
在处理大型 JSX 代码时,将某些内容块提取为单独的函数来渲染组件的一部分是很有帮助的,类似于将大型函数分解为多个较小的函数。
function?Article(props:?Props)?{
??//?...
??return?(
????<article>
??????<h1>{props.title}</h1>
??????{renderBody()}
??????{renderFooter()}
????</article>
??);
??function?renderBody()?{
????return?/*?article?body?JSX?*/;
??}
??function?renderFooter()?{
????return?/*?article?footer?JSX?*/;
??}
}
export?default?Article;
可以给这些拆分出来的函数前面加上render
前缀,以将它们与其他不返回 JSX 的函数区分开;
可以将这些函数放在return
语句之后,以便将与内容相关的所有内容组合在一起;
无需向这些函数传递任何参数,因为它们可以访问props
和组件定义的所有变量;
那为什么不将它们提取为组件呢?关于部分渲染函数其实是存在争议的,一种说法是要避免从组件内定义的任何函数中返回 JSX,另一种说法是将这些函数提取为单独的组件。
function?Article(props:?Props)?{
??//?...
??return?(
????<article>
??????<h1>{props.title}</h1>
??????<ArticleBody?{...props}?/>
??????<ArticleFooter?{...props}?/>
????</article>
??);
}
export?default?Article;
function?ArticleBody(props:?Props)?{}
function?ArticleFooter(props:?Props)?{}
在这种情况下,就必须手动将子组件所需的局部变量通过props
传递。在使用 TypeScript 时,我们还需要为组件的props
定义额外的类型。最终代码就会变得臃肿,这就会导致代码变得难以阅读和理解:
function?Article(props:?Props)?{
??const?[status,?setStatus]?=?useState("");
??return?(
????<article>
??????<h1>{props.title}</h1>
??????<ArticleBody?{...props}?status={status}?/>
??????<ArticleFooter?{...props}?setStatus={setStatus}?/>
????</article>
??);
}
export?default?Article;
interface?BodyProps?extends?Props?{
??status:?string;
}
interface?FooterProps?extends?Props?{
??setStatus:?Dispatch<SetStateAction<string>>;
}
function?ArticleBody(props:?BodyProps)?{}
function?ArticleFooter(props:?FooterProps)?{}
这些单独的组件不可以重复使用,它们仅被它们所属的组件使用,单独使用它们是没有意义的。因此,这种情况下,还是建议将部分 JSX 提取成渲染函数。
React 组件通常会包含事件处理函数,它们是嵌套函数,通常会更改组件的内部状态或调度操作以更新组件的状态。
另一类嵌套函数就是闭包,它们是读取组件状态或props
的不纯函数,用于构建组件逻辑。
function?Article(props:?Props)?{
??const?[email,?setEmail]?=?useState("");
??return?(
????<article>
??????{/*?...?*/}
??????<form?onSubmit={subscribe}>
????????<input?type="email"?value={email}?onChange={setEmail}?/>
????????<button?type="submit">Subscribe</button>
??????</form>
????</article>
??);
??//?事件处理
??function?subscribe():?void?{
????if?(canSubscribe())?{
??????//?发送订阅请求
????}
??}
??function?canSubscribe():?boolean?{
????//?基于?props?和?state?的逻辑
??}
}
export?default?Article;
通常会使用函数声明而不是函数表达式来声明函数,因为函数是存在提升的,这允许我们在使用它们之后定义它们。这样就可以将它们放在组件函数的末尾,return
语句之后;
如果一个函数中嵌套了另外一个函数,那么建议将调用者放在被调用者之前;
将这些函数按使用顺序排列。
最后就是纯函数,我们可以将它们放在组件文件的底部,在 React 组件之外:
function?Article(props:?Props)?{
??//?...
??//???纯函数不应该放在组件之中
??function?getInitials(str:?string)?{}
}
export?default?Article;
function?Article(props:?Props)?{
??//?...
}
//???纯函数应该放在组件之外
function?getInitials(str:?string)?{}
export?default?Article;
首先,纯函数没有依赖项,如 props、状态或局部变量,它们接收所有依赖项作为参数。这意味着可以将它们放在任何地方。但是,将它们放在组件之外还有其他原因:
它向任何阅读代码的开发人员发出信号,表示它们是纯粹的;
它们很容易测试,只需要将要测试的函数导出并导入到测试文件中即可;
如果需要提取和重用它们,可以很容易将它们很移动到其他文件。
下面是一个完整的典型 React 组件示例。由于重点是文件的结构,因此省略了实现细节。
//?1???导入依赖项
import?React?from?"react";
import?{?Tag?}?from?"./tag";
import?styles?from?"./article.module.scss";
//?2???静态定义
const?MAX_READING_TIME?=?10;
interface?Props?{
??id:?number;
??name:?string;
??title:?string;
??meta:?Metadata;
}
//?3???组件定义
function?Article(props:?Props)?{
??//?4???变量定义
??const?router?=?useRouter();
??const?theme?=?useTheme();
??const?{?id,?title,?content,?onSubscribe?}?=?props;
??const?{?image,?author,?date?}?=?meta;
??const?[email,?setEmail]?=?React.useState("");
??const?[showMenu,?toggleMenu]?=?React.useState(false);
??const?summary?=?React.useMemo(()?=>?getSummary(content),?[content]);
??const?initials?=?getInitials(author);
??const?formattedDate?=?getDate(date);
??//?5???effects
??React.useEffect(()?=>?{
????//?...
??},?[]);
??//?6???渲染内容
??return?(
????<article>
??????<h1>{title}</h1>
??????{renderBody()}
??????<form?onSubmit={subscribe}>
????????{renderSubscribe()}
??????</form>
????</article>
??);
??//?7???部分渲染
??function?renderBody()?{?/*...*/?}
??function?renderSubscribe()?{?/*...*/?}
??//?8???局部函数
??function?subscribe()?{?/*...*/?}
}
//?9???纯函数
function?getInitials(str:?string)?{?/*...*/?}
export?default?Article;
添加好友备注【进阶学习】拉你进技术交流群