查看如何组织更优雅的React组件!1.导入依赖我们通常会在组件文件的最前面导入组件需要的依赖。对于不同类别的依赖,建议将它们分组,这样有助于我们更好地理解组件。导入的依赖可以分为四类://外部依赖importReactfrom"react";import{useRouter}from"next/router";//内部依赖import{Button}from"../src/components/button";//本地依赖项import{Tag}from"./tag";import{Subscribe}from"./subscribe";//样式importstylesfrom"./article.module.scss";Externaldependencies:主要是外部依赖,这些依赖定义在package.json文件中,从node_modules导入;内部依赖:内部依赖主要是位于组件文件夹外的可重用组件或模块,这些导入应该使用相对导入语法,以...开头。通常,大多数导入的依赖项都属于这一类。因此,如果有必要,我们可以进一步分离这类组件,例如:UI组件、数据相关的导入、服务等;本地依赖项:本地依赖项主要是与组件位于同一文件夹中的依赖项或子组件。这些依赖项的所有导入路径都应以./开头。主要是较大的组件将包含本地依赖项;样式:这最后一部分在大多数情况下只包含一个导入-样式文件。如果导入多个样式表,需要考虑样式拆分是否有问题。手动对导入的依赖进行分组可能会很麻烦,而Prettier可以帮助我们自动格式化代码。prettier-plugin-sort-imports插件可用于自动格式化依赖导入。需要在项目根目录下创建一个prettier.config.js配置文件,并在里面配置规则:module.exports={//Prettier的其他配置importOrder:[//默认情况下,外部依赖会放在最前面//内部依赖项"^../(.*)",//局部依赖项,样式除外"^./((?!scss).)*$",//其他"^./(.*)",],importOrderSeparation:true,};下面是插件官方给的例子,输入如下:importReact,{FC,useEffect,useRef,ChangeEvent,KeyboardEvent,}from'react';从'@core/logger'导入{记录器};import{reduce,debounce}from'lodash';import{Message}from'../Message';import{createServer}from'@server/node';import{Alert}from'@ui/Alert';import{repeat,过滤,添加}from'../utils';import{initializeApp}from'@core/app';import{Popup}from'@ui/Popup';import{createConnection}from'@server/database';formatdebounce后的输出如下:import{debounce,reduce}from'lodash';importReact,{ChangeEvent,FC,KeyboardEvent,useEffect,useRef,}from'react';import{createConnection}from'@server/database';从'@导入{createServer}服务器/节点';从'@core/app'导入{initializeApp};从'@core/logger'导入{logger};从'@ui/Alert'导入{Alert};从'@ui/导入{Popup}弹出窗口';从'../Message'导入{Message};从'../utils'导入{添加,过滤,重复};更漂亮的插件排序导入:https://github.com/trivago/prettier-plugin-sort-imports2。静态定义在importdependencies下面,通常放文件级别的常量和TypeScript或Flow等静态类型检查器定义的类型定义(1)常量定义组件中的所有magic值,比如Strings或numbers,应该放在文件的顶部,在导入的依赖项下面。由于这些是静态常量,这意味着它们的值不会改变。所以将它们放在组件中是没有意义的,因为如果将它们放在组件中,每次重新渲染组件时都会重新创建它们。常量MAX_READING_TIME=10;constMETA_TITLE="你好世界";对于更复杂的静态数据结构,可以将其提取到单独的文件中以保持组件代码的清洁。(2)类型定义下面是使用TypeScript声明的组件props的类型:interfaceProps{id:number;名称:字符串;标题:字符串;meta:Metadata;}如果不需要导出这个props的类型,Props可以作为一个接口名,帮助我们立刻识别一个组件props的类型定义,和其他类型区分开来。只有当这个props类型需要在多个组件中使用时,才需要加上组件名称,比如ButtonProps,因为当它引入另一个组件时,不应该和另一个组件的props类型冲突。3.组件定义函数组件的定义有两种方式:函数声明和箭头函数。推荐使用函数声明的形式,因为这是语法声明的内容:函数。官方文档中的例子中也使用了这个方法:functionArticle(props:Props){/**/}箭头函数只在必须使用forwardRef的时候使用:constArticle=React.forwardRef((props,ref)=>{/**/});通常,组件默认在组件末尾导出:exportdefaultArticle;4.变量声明接下来,我们需要在组件中声明变量。请注意,即使使用const声明,这些在这里也称为变量,因为它们的值通常在渲染之间发生变化,并且仅在执行单个渲染过程时保持不变。const{id,name,title}=props;const路由器=useRouter();const首字母=getInitials(名字);这通常包含在组件级别使用的所有变量,使用const或let定义,具体取决于它们在render期间是否更改其值:解构数据:通常来自props、数据存储或组件状态;Hooks:自定义Hooks,框架内置Hooks,如useState、useReducer、useRef、useCallback或useMemo;整个组件使用的处理数据,由函数计算;一些较大的组件可能需要在组件内声明许多变量。在这种情况下,建议根据它们的初始化方法或用途对它们进行分组://frameworkhooksconstrouter=useRouter();//自定义钩子constuser=useLoggedUser();consttheme=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);//内存数据constsubscribe=React.useCallback(onSubscribe,[id]);constsummary=React.useMemo(()getSummary(content),[content]);//refsconstsideMenuRef=useRef(null);constsubscribeRef=useRef(null);//计算数据constinitials=getInitials(author);constformattedDate=getDate(日期);根据变量的数量和类型,分组变量的方法在组件之间可能有很大差异。关键是将相关的变量组合在一起,不同组之间加一个空行,提高代码的可读性。注意:以上代码中的注释仅用于标记分组类型,实际项目中不会写这些注释。5.EffectsEffects部分通常写在变量声明之后。它们可能是React中最复杂的构造,但从语法的角度来看它们非常简单:useEffect((){setLogo(theme==="dark"?"white":"black");},[theme]);任何包含在效果中但定义在效果之外的变量都应该包含在dependencies数组中。另外,应该使用return来清理副作用:useEffect((){functiononScroll(){/*...*/}window.addEventListener("scroll",onScroll);return()window.removeEventListener("滚动”,onScroll);},[]);6.渲染内容组件的核心是它的内容。React组件的内容使用JSX语法定义,并在浏览器中呈现为HTML。因此,建议将函数组件的返回语句尽量放在靠近文件顶部的位置。其他一切都只是细节,应该放在文件的下方。functionArticle(props:Props){//变量声明//效果//?自定义函数不建议放在返回部分之前functiongetInitials(){/*...*/}return/*content*/;}exportdefaultArticle;functionArticle(props:Props){//变量声明//效果return/*content*/;//?自定义函数建议放在返回部分functiongetInitials(){/*...*/}}exportdefaultArticle;return不应该放在函数的末尾吗?事实上,并非如此。对于常规函数,return必须放在末尾。然而,React组件不是简单的函数,它们通常包含具有各种用途的嵌套函数,例如事件处理程序。最后的return语句,连同前面的一堆其他函数,实际上阻碍了代码的阅读,使得很难找到组件呈现的内容:很难搜索那个return语句,因为可能有多个return语句来自其他嵌套函数;滚动文件末尾找到返回语句并不容易,因为返回的JSX块可能非常大。当然函数定义的位置可以根据个人喜好来定。如果把函数放在return下面,那么如果要用箭头函数来定义函数,只能用var来定义,因为let和const没有变量提升,不能在函数前面使用定义的箭头函数。7.局部渲染在处理大型JSX代码时,将某些内容块提取为单独的函数来渲染组件的各个部分会很有帮助,类似于将一个大型函数分解为多个较小的函数。functionArticle(props:Props){//...return({props.title}
{renderBody()}{renderFooter()});functionrenderBody(){return/*文章主体JSX*/;}functionrenderFooter(){return/*文章页脚JSX*/;}}导出默认文章;这些拆分函数可以加上render前缀,以区别于其他不返回JSX的函数;这些函数可以放在return语句之后,以便组合与内容相关的所有内容;不需要向这些函数传递任何参数,因为它们可以访问props和组件定义的所有变量;那么为什么不将它们提取为组件呢?关于部分渲染函数实际上存在争论,一个是避免从组件内部定义的任何函数返回JSX,另一个是将这些函数提取到一个单独的组件中。functionArticle(props:Props){//...return({props.title}
);}exportdefaultArticle;functionArticleBody(props:Props){}functionArticleFooter(props:Props){}这种情况下,你必须通过props手动传递子组件需要的局部变量。在使用TypeScript时,我们还需要为组件props定义额外的类型。最终代码变得臃肿,导致代码难以阅读和理解:functionArticle(props:Props){const[status,setStatus]=useState("");返回(>);}exportdefaultArticle;interfaceBodyPropsextendsProps{status:string;}interfaceFooterPropsextendsProps{setStatus:Dispatch>;}functionArticleBody(props:BodyProps){}函数ArticleFooter(props:FooterProps){}这些单独的组件不能重复使用,它们只被所属的组件使用,单独使用没有意义。因此,在这种情况下,建议将部分JSX提取到一个渲染函数中。8.本地函数React组件通常会包含事件处理程序,这些事件处理程序是嵌套函数,通常会更改组件的内部状态或调度操作以更新组件的状态。另一类嵌套函数是闭包,它们是读取组件状态或道具的非纯函数,用于构建组件逻辑。functionArticle(props:Props){const[email,setEmail]=useState("");return({/*...*/}订阅);//事件处理functionsubscribe():void{if(canSubscribe()){//发送订阅请求}}functioncanSubscribe():boolean{//基于props和state的逻辑}}exportdefaultArticle;通常使用函数声明代替函数表达式来声明函数,因为函数被提升了,这允许我们在使用它们之后定义它们。这样,它们就可以放在组件函数的末尾,return语句之后;如果一个函数中嵌套了另一个函数,建议将调用者放在被调用者之前;按照使用顺序排列这些函数。9.purefunctions就是最后的纯函数,我们可以把它们放在组件文件的底部,React组件外面:组件之间InfunctiongetInitials(str:string){}}exportdefaultArticle;functionArticle(props:Props){//...}//?纯函数应该放在组件之外functiongetInitials(str:string){}导出默认文章;首先,纯函数没有诸如props、state或局部变量之类的依赖项,它们接收所有依赖项作为参数。这意味着它们可以放置在任何地方。然而,将它们放在组件之外还有其他原因:它向任何阅读代码的开发人员发出信号,表明它们是纯净的;是的;如果您需要提取和重新使用它们,可以轻松地将它们移动到其他文件中。完整示例下面是一个典型React组件的完整示例。由于重点放在文件的结构上,因此省略了实现细节。//1??导入依赖项importReactfrom"react";import{Tag}from"./tag";importstylesfrom"./article.module.scss";//2??静态定义constMAX_READING_TIME=10;interfaceProps{id:数字;名称:字符串;标题:字符串;meta:Metadata;}//3??组件定义函数Article(props:Props){//4??变量定义constrouter=useRouter();consttheme=useTheme();const{id,title,content,onSubscribe}=props;const{图片、作者??、日期}=meta;const[email,setEmail]=React.使用状态(“”);const[showMenu,toggleMenu]=React.使用状态(假);constsummary=React.useMemo(()getSummary(content),[content]);constinitials=getInitials(作者);constformattedDate=getDate(日期);//5??效果React.useEffect((){//...},[]);//6??渲染内容返回({title}
{renderBody()}{renderSubscribe()});//7??局部渲染函数nrenderBody(){/*...*/}functionrenderSubscribe(){/*...*/}//8??局部函数functionsubscribe(){/*...*/}}//9??纯函数functiongetInitials(str:string){/*...*/}导出默认文章;