网站/小程序/APP个性化定制开发,二开,改版等服务,加扣:8582-36016

我们从代码结构的角度来谈谈如何设计一个更优雅的 React 组件。优秀的组件有着一个清晰的目录结构。这里的目录结构分为项目级结构、单组件级结构。

在日常团队开发中大家写的组件质量参差不齐,风格千差万别。会因为很多需求导致组件无法扩展,或难以维护。导致很多业务组件的功能重复,使用起来相当难受。我们从代码结构的角度来谈谈如何设计一个更优雅的 React 组件。

组件目录结构

优秀的组件有着一个清晰的目录结构。这里的目录结构分为项目级结构、单组件级结构。

容器组件/展示组件

在项目中我们的目录结构可以根据组件和业务耦合来划分,和业务的耦合程度越低, 可复用性越强。展示组件只关注展示层, 可以在多个地方被复用, 它不耦合业务。容器组件主要关注业务处理,容器组件通过组合展示组件来构建完整视图。

示例:

src/ 
  components/ (通用组件,与业务无关,可被其他所有组件调用) 
    Button/ 
      index.tsx 
  containers/ (容器组件,与业务深度耦合,可被页面组件调用) 
    Hello/ 
      Kitty/ (容器组件中的特有组件,不能与其他容器组件共享) 
      index.tsx 
    World/ 
      components/ 
      index.tsx 
  hooks/ (公共的 hooks) 
  pages/ (页面组件,特定的页面,无复用性) 
    my-app/ 
  store/ (状态管理) 
  services/ (接口定义) 
  utils/ (工具类)

    组件目录结构

    我们可以根据文件类型/功能/职责等划分不同的目录。

    1. 根据文件类型可以分出 images 等目录

    2. 根据文件功能可以分出 __tests__ 、demo 等目录

    3. 根据文件职责可以分出 types 、utils 、hooks 等目录

    4. 根据组件的特点可以用目录划分归类

    HelloWorld/ (普通的业务组件) 
      __tests__/ (测试用例) 
      demo/ (组件示例) 
      Bar/ (特有组件分类) 
        Kitty.tsx (特有组件) 
        Kitty.module.less 
      Foo/ 
      hooks/ (自定义 hooks) 
      images/ (图片目录) 
      types/ (类型定义) 
      utils/ (工具类方法) 
      index.tsx (出口文件)

      比如我最近写的一个表格组件的目录结构:

      ├─SheetTable 
      │  ├─Cell 
      │  ├─Header 
      │  ├─Layer 
      │  ├─Main 
      │  ├─Row 
      │  ├─Store 
      │  ├─types 
      │  └─utils

        组件内部结构

        组件内部需要保持良好的顺序逻辑,统一团队规范。约定俗成后,这样一目了然定义可以让我们更清晰地去 Review。

        导入顺序

        导入顺序为 node_modules -> @/ 开头文件 -> 相对路径文件 -> 当前组件样式文件

        // 导入 node_modules 依赖 
        import React from'react'; 
        // 导入公共组件 
        import Button from'@/components/Button'; 
        // 导入相对路径组件 
        import Foo from'./Foo'; 
        // 导入对应同名的 .less 文件,命名为 styles 
        import styles from'./Kitty.module.less';

          使用 组件名 + Props 形式命名 Props 类型并导出。

          类型与参数书写的顺序保持一致,一般以 [a-z] 的顺序定义。变量的注释禁止放末尾,原因是会导致编辑器识别错位,无法正确提示

          /** 
           * 类型定义(命名:组件名 + Props) 
           */ 
          export interface KittyProps { 
            /** 
             * 多行注释(建议) 
             */ 
            email: string; 
            // 单行注释(不推荐) 
            mobile: string; 
            username: string; // 末尾注释(禁止) 
          }

            使用 React.FC 定义


            const Kitty: React.FC<KittyProps> = ({ email, mobile, usename }) => {};

              泛型,代码提示更智能

              以下例子,可以用过泛型让 value 和 onChange 回调中的类型保持一致,并做到编辑器智能类型提示。

              注意:泛型组件无法使用 React.FC 类型

              export interface FooProps<Value> { 
                value: Value; 
                onChange: (value: Value) =>void; 
              } 
              
              exportfunction Foo<Value extends React.Key>(props: FooProps<Value>) {}


                禁止直接使用 any 类型

                无论隐式和显式的方式,都不推荐使用 any 类型。定义了 any 的参数会让使用该组件的人产生极度困惑,无法明确地知道其中的类型。我们可以通过泛型的方式去声明。

                // 隐式 any (禁止) 
                let foo; 
                function bar(param) {} 
                
                // 显式 any (禁止) 
                let hello: any; 
                function world(param: any) {} 
                
                // 使用泛型继承,缩小类型范围 (推荐) 
                function Tom<P extends Record<string, any>>(param: P) {}

                  一个组件对应一个样式文件

                  我们以组件的颗粒度大小为抽象单元,样式文件则应与组件本身保持一致。不推荐交叉引入样式文件的做法,这样会导致重构混乱,无法明确当前这个样式被多少个组件使用。

                  - Tom.tsx 
                  - Tom.module.less 
                  - Kitty.tsx 
                  - Kitty.module.less

                    内联样式

                    避免偷懒,要时刻保持优雅,随手一个 style={} 是极为不推荐的。这样不仅每次渲染都有重新创建的消耗,而且是清晰的 JSX 上的噪点,影响阅读。

                    组件行数限制

                    组件需要明确的注释,并保持 300 行以内的代码行数。代码行数可以通过配置 eslint 来做到限制(可以跳过注释/空行的的统计):

                    'max-lines-per-function': [2, { max: 320, skipComments: true, skipBlankLines: true }],

                      组件内部编写代码的顺序

                      组件内部的顺序为 state -> custom Hooks -> effects -> 内部 function -> 其他逻辑 -> JSX

                      /** 
                       * 组件注释(简明概要) 
                       */ 
                      const Kitty: React.FC<KittyProps> = ({ email }) => { 
                        // 1. state 
                      
                        // 2. custom Hooks 
                      
                        // 3. effects 
                      
                        // 4. 内部 function 
                      
                        // 5. 其他逻辑... 
                      
                        return ( 
                          <div className={styles.wrapper}> 
                            {email} 
                            <Child /> 
                          </div> 
                        ); 
                      };

                        事件函数命名区分

                        内部方法按照 handle{Type}{Event} 命名,例如 handleNameChange。暴露外部的方法按照 on{Type}{Event},例如 onNameChange。这样做的好处可以直接通过函数名区分是否为外部参数。

                        例如 antd/Button 组件片段:

                        const handleClick = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => { 
                          const { onClick, disabled } = props; 
                          if (innerLoading || disabled) { 
                            e.preventDefault(); 
                            return; 
                          } 
                          (onClick as React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>)?.(e); 
                        };


                          继承原生元素 props 定义

                          原生元素 props 都继承了 React.HTMLAttributes。某些特殊元素也会扩展自己的属性,例如 InputHTMLAttributes。

                          我们定义一个自定义组件则可以通过继承 React.InputHTMLAttributes ,让其类型具有所有 input 的特性。

                          export interface KittyProps extends React.InputHTMLAttributes<HTMLInputElement> { 
                            /** 
                             * 新增支持回车键事件 
                             */ 
                            onPressEnter?: React.KeyboardEventHandler<HTMLInputElement>; 
                          } 
                          
                          function Kitty({ onPressEnter, onKeyUp, ...restProps }: KittyProps) { 
                            function handleKeyUp(e: React.KeyboardEvent<HTMLInputElement>) { 
                              if (e.code.includes('Enter') && onPressEnter) { 
                                onPressEnter(e); 
                              } 
                              if (onKeyUp) { 
                                onKeyUp(e); 
                              } 
                            } 
                          
                            return<input onKeyUp={handleKeyUp} {...restProps} />; 
                          }

                            避免循环依赖

                            如果你写的组件包含了循环依赖, 这时候你需要考虑拆分和设计模块文件

                            // --- Foo.tsx --- 
                            import Bar from'./Bar'; 
                            
                            export interface FooProps {} 
                            
                            exportconst Foo: React.FC<FooProps> = () => {}; 
                            Foo.Bar = Bar; 
                            
                            // --- Bar.tsx ---- 
                            import { FooProps } from'./Foo';


                              上面 Foo 和 Bar 组件就形成了一个简单循环依赖, 尽管它不会造成什么运行时问题. 解决方案就是将 FooProps 抽取到单独的文件:

                              // --- types.ts --- 
                              export interface FooProps {} 
                              
                              // --- Foo.tsx --- 
                              import Bar from'./Bar'; 
                              import { FooProps } from'./types'; 
                              
                              exportconst Foo: React.FC<FooProps> = () => {}; 
                              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 的项目管理方式来解耦各个组件。只要搭建一套脚手架,就能管理(构建、测试、发布)多个 package

                                        不要直接使用 export default 导出未命名的组件

                                        这种方式导出的组件在 React Inspector 查看时会显示为 Unknown

                                        // 错误做法 
                                        exportdefault () => {}; 
                                        
                                        // 正确做法 
                                        exportdefaultfunction Kitty() {} 
                                        
                                        // 正确做法:先声明后导出 
                                        function Kitty() {} 
                                        
                                        exportdefault Kitty;

                                          结语

                                          以上是写 React 组件在目录结构以及编码规则上需要注意的点,后续我们讲解如何在思维上保持优雅。


                                          评论 0

                                          暂无评论
                                          0
                                          0
                                          0
                                          立即
                                          投稿
                                          发表
                                          评论
                                          返回
                                          顶部