当前位置: 首页 > 科技观察

如何编写更优雅的React组件——代码结构

时间:2023-03-22 13:29:38 科技观察

在日常团队开发中,每个人编写的组件质量和风格各不相同。由于需求较多,组件无法扩展和维护。导致很多业务组件功能重复,使用起来相当不爽。下面我们就从代码结构的角度谈谈如何设计一个更优雅的React组件。组件目录结构好的组件都有清晰的目录结构。这里的目录结构分为项目级结构和单组件级结构。容器组件/展示组件在项目中,我们的目录结构可以按照组件和业务耦合来划分。与业务的耦合度越低,复用性越强。展示组件只关注展示层,可以在不耦合业务的情况下,在多处复用。容器组件主要侧重于业务处理,容器组件通过组合显示组件构建一个完整的视图。示例:src/components/(普通组件,与业务无关,其他所有组件均可调用)Button/index.tsxcontainers/(容器组件,与业务深度耦合,可被页面组件调用)Hello/Kitty/(容器component唯一组件,不能与其他容器组件共享)index.tsxWorld/components/index.tsxhooks/(publichooks)pages/(pagecomponents,specificpages,noreusability)my-app/store/(statemanagement)services/(接口定义)utils/(工具类)组件目录结构我们可以根据文件类型/功能/职责等划分不同的目录。图片等目录可以根据文件类型进行分隔。__tests__、demo等目录可以根据文件功能分开。types、utils、hooks等目录可以根据文件职责分开。根据组件的特点,目录如HelloWorld/(普通业务Component)__tests__/(测试用例)demo/(组件示例)Bar/(具体组件分类)Kitty.tsx(具体组件)Kitty.module.lessFoo/hooks/(customhooks)images/(imagedirectory)types/(Typedefinition)utils/(toolclassmethod)index.tsx(exportfile)比如我最近写的一个表格组件的目录结构:├─SheetTable│├─Cell│├─Header│├─Layer│├─Main│├─Row│├─Store│├─types│└─utils组件内部结构组件需要保持良好的顺序逻辑,统一团队规范。公约确立后,如此明确的定义,让我们可以更清楚地检讨。导入顺序导入顺序为node_modules->@/开头文件->相对路径文件->当前组件样式文件//导入node_modules依赖importReactfrom'react';//导入公共组件importButtonfrom'@/components/Button';//ImportRelativepathcomponentimportFoofrom'./Foo';//导入同名对应的.less文件,命名为stylesimportstylesfrom'./Kitty.module.less';使用组件名称+Props命名Props类型并导出。类型和参数的书写顺序是一致的,一般按照[a-z]的顺序定义。禁止把变量的注释放在最后,因为这样会导致编辑器识别错位,无法正确提示/***typedefinition(name:componentname+Props)*/exportinterfaceKittyProps{/***多行注释(建议)*/email:string;//单行注释(不推荐)mobile:string;username:string;//结束注释(禁止)}使用React.FC定义constKitty:React.FC=({email,mobile,usename})=>{};泛型,更智能的代码提示在下面的例子中,您可以使用泛型来保持value和onChange回调中的类型一致,并在编辑器中实现智能类型提示。注意:通用组件不能使用React.FC类型exportinterfaceFooProps{value:Value;onChange:(value:Value)=>void;}exportfunctionFoo(props:FooProps){}来禁止direct使用any类型any类型已被隐式和显式弃用。定义为any的参数会给使用该组件的人带来极大的混乱,无法清楚地知道类型。我们可以用通用的方式声明它。//Implicitany(prohibited)letfoo;functionbar(param){}//Explicitany(prohibited)lethello:any;functionworld(param:any){}//使用泛型继承缩小类型范围(推荐)functionTom>(param:P){}一个组件对应一个样式文件。我们以组件的粒度为抽象单位,样式文件要与组件本身保持一致。不建议交叉引入样式文件,这样会导致重构时出现混乱,无法知道当前样式被多少组件使用。-Tom.tsx-Tom.module.less-Kitty.tsx-Kitty.module.less内联样式避免偷懒,时刻保持优雅,非常不推荐随意的style={}。这样一来,不仅每次渲染都有重新创建的成本,而且清晰的JSX上的噪音影响阅读。ComponentLineLimitComponents需要显式注释,代码行数控制在300行以下,可以通过配置eslint来限制代码行数(可以跳过comments/blanklines的统计):'max-lines-per-function':[2,{max:320,skipComments:true,skipBlankLines:true}],组件内部写代码的顺序组件内部的顺序是state->customHooks->effects->internalfunction->otherlogic->JSX/***组件注解(简洁大纲)*/constKitty:React.FC=({email})=>{//1.state//2.customHooks//3.effects//4.内部功能//5。其他逻辑...return({email}

);};事件函数命名区分内部方法根据handle{Type}{Event}命名,比如作为handleNameChange。外部方法根据on{Type}{Event}暴露,比如onNameChange。这样做的好处是可以直接通过函数名来区分是否是外部参数。例如antd/Button组件片段:constandleClick=(e:React.MouseEvent)=>{const{onClick,disabled}=props;if(innerLoading||disabled){e.preventDefault();返回;}(onClickasReact.MouseEventHandler)?.(e);};继承原生元素props定义原生元素props来继承React.HTMLAttributes。某些特殊元素还扩展了它们自己的属性,例如InputHTMLAttributes。当我们定义一个自定义组件时,我们可以继承React.InputHTMLAttributes使其类型具有输入的所有特性。exportinterfaceKittyPropsextendsReact.InputHTMLAttributes{/***对输入键事件的新支持*/onPressEnter?:React.KeyboardEventHandler;}functionKitty({onPressEnter,onKeyUp,...restProps}:KittyProps){functionhandleKeyUp(e:React.KeyboardEvent){if(e.code.includes('Enter')&&onPressEnter){onPressEnter(e);}if(onKeyUp){onKeyUp(e);}}返回;}避免循环依赖如果你写的组件包含循环依赖,你需要考虑拆分和设计模块文件//---Foo.tsx---importBarfrom'./Bar';exportinterfaceFooProps{}exportconstFoo:React.FC=()=>{};Foo.Bar=Bar;//---Bar.tsx----import{FooProps}from'./Foo';上面的Foo和Bar组件形成了一个简单的循环依赖,尽管它不会导致任何运行时问题。解决方案是将FooProps提取到一个单独的文件中://---types.ts---exportinterfaceFooProps{}//---Foo.tsx---importBarfrom'./Bar';import{FooProps}from'。/types';exportconstFoo:React.FC=()=>{};Foo.Bar=Bar;//---Bar.tsx----import{FooProps}from'./types';相对路径不要超过两级当项目复杂时,目录结构会变得更复杂再深入一点,文件会有一个很长的../路径,看起来很不优雅:import{ButtonProps}from'../../../components/Button';我们可以通过在tsconfig.json中配置"paths":{"@/*":["src/*"]}在vite中配置alias:{'@/':`${path.resolve(process.cwd(),'src')}/`,}现在我们可以导入相对于src的模块:import{ButtonProps}from'@/components/Button';当然更彻底一些,我们可以使用monorepo的项目管理方式,对各个组件进行解耦。只需搭建一套脚手架,即可管理(构建、测试、发布)多个包。不要直接使用exportdefault来导出未命名的组件。这样导出的组件在ReactInspector查看时会显示为Unknown//错误的方式exportdefault()=>{};//正确做法exportdefaultfunctionKitty(){}//正确做法:先声明再导出functionKitty(){}exportdefaultKitty;结束语以上是在目录结构和编码规则方面编写React组件需要注意的点。稍后,我们将解释如何用优雅的方式思考。