当前位置:首页 > 应用开发

如何写出更优雅的 React 组件 - 代码结构篇

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

组件目录结构

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

容器组件/展示组件

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

示例:

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

组件目录结构

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

根据文件类型可以分出 images 等目录 根据文件功能可以分出 __tests__ 、demo 等目录 根据文件职责可以分出 types 、utils 、hooks 等目录 根据组件的特点可以用目录划分归类 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 fromreact; // 导入公共组件 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 组件在目录结构以及编码规则上需要注意的点,后续我们讲解如何在思维上保持优雅。

分享到:

滇ICP备2023006006号-16