如何寫出更優(yōu)雅的 React 組件 - 代碼結(jié)構(gòu)篇
在日常團(tuán)隊(duì)開發(fā)中大家寫的組件質(zhì)量參差不齊,風(fēng)格千差萬別。會因?yàn)楹芏嘈枨髮?dǎo)致組件無法擴(kuò)展,或難以維護(hù)。導(dǎo)致很多業(yè)務(wù)組件的功能重復(fù),使用起來相當(dāng)難受。我們從代碼結(jié)構(gòu)的角度來談?wù)勅绾卧O(shè)計(jì)一個(gè)更優(yōu)雅的 React 組件。
組件目錄結(jié)構(gòu)
優(yōu)秀的組件有著一個(gè)清晰的目錄結(jié)構(gòu)。這里的目錄結(jié)構(gòu)分為項(xiàng)目級結(jié)構(gòu)、單組件級結(jié)構(gòu)。
容器組件/展示組件
在項(xiàng)目中我們的目錄結(jié)構(gòu)可以根據(jù)組件和業(yè)務(wù)耦合來劃分,和業(yè)務(wù)的耦合程度越低, 可復(fù)用性越強(qiáng)。展示組件只關(guān)注展示層, 可以在多個(gè)地方被復(fù)用, 它不耦合業(yè)務(wù)。容器組件主要關(guān)注業(yè)務(wù)處理,容器組件通過組合展示組件來構(gòu)建完整視圖。
示例:
- src/
- components/ (通用組件,與業(yè)務(wù)無關(guān),可被其他所有組件調(diào)用)
- Button/
- index.tsx
- containers/ (容器組件,與業(yè)務(wù)深度耦合,可被頁面組件調(diào)用)
- Hello/
- Kitty/ (容器組件中的特有組件,不能與其他容器組件共享)
- index.tsx
- World/
- components/
- index.tsx
- hooks/ (公共的 hooks)
- pages/ (頁面組件,特定的頁面,無復(fù)用性)
- my-app/
- store/ (狀態(tài)管理)
- services/ (接口定義)
- utils/ (工具類)
組件目錄結(jié)構(gòu)
我們可以根據(jù)文件類型/功能/職責(zé)等劃分不同的目錄。
- 根據(jù)文件類型可以分出 images 等目錄
- 根據(jù)文件功能可以分出 __tests__ 、demo 等目錄
- 根據(jù)文件職責(zé)可以分出 types 、utils 、hooks 等目錄
- 根據(jù)組件的特點(diǎn)可以用目錄劃分歸類
- HelloWorld/ (普通的業(yè)務(wù)組件)
- __tests__/ (測試用例)
- demo/ (組件示例)
- Bar/ (特有組件分類)
- Kitty.tsx (特有組件)
- Kitty.module.less
- Foo/
- hooks/ (自定義 hooks)
- images/ (圖片目錄)
- types/ (類型定義)
- utils/ (工具類方法)
- index.tsx (出口文件)
比如我最近寫的一個(gè)表格組件的目錄結(jié)構(gòu):
- ├─SheetTable
- │ ├─Cell
- │ ├─Header
- │ ├─Layer
- │ ├─Main
- │ ├─Row
- │ ├─Store
- │ ├─types
- │ └─utils
組件內(nèi)部結(jié)構(gòu)
組件內(nèi)部需要保持良好的順序邏輯,統(tǒng)一團(tuán)隊(duì)規(guī)范。約定俗成后,這樣一目了然定義可以讓我們更清晰地去 Review。
導(dǎo)入順序
導(dǎo)入順序?yàn)?node_modules -> @/ 開頭文件 -> 相對路徑文件 -> 當(dāng)前組件樣式文件
- // 導(dǎo)入 node_modules 依賴
- import React from'react';
- // 導(dǎo)入公共組件
- import Button from'@/components/Button';
- // 導(dǎo)入相對路徑組件
- import Foo from'./Foo';
- // 導(dǎo)入對應(yīng)同名的 .less 文件,命名為 styles
- import styles from'./Kitty.module.less';
使用 組件名 + Props 形式命名 Props 類型并導(dǎo)出。
類型與參數(shù)書寫的順序保持一致,一般以 [a-z] 的順序定義。變量的注釋禁止放末尾,原因是會導(dǎo)致編輯器識別錯(cuò)位,無法正確提示
- /**
- * 類型定義(命名:組件名 + Props)
- */
- export interface KittyProps {
- /**
- * 多行注釋(建議)
- */
- email: string;
- // 單行注釋(不推薦)
- mobile: string;
- username: string; // 末尾注釋(禁止)
- }
使用 React.FC 定義
- const Kitty: React.FC<KittyProps> = ({ email, mobile, usename }) => {};
泛型,代碼提示更智能
以下例子,可以用過泛型讓 value 和 onChange 回調(diào)中的類型保持一致,并做到編輯器智能類型提示。
注意:泛型組件無法使用 React.FC 類型
- export interface FooProps<Value> {
- value: Value;
- onChange: (value: Value) =>void;
- }
- exportfunction Foo<Value extends React.Key>(props: FooProps<Value>) {}
禁止直接使用 any 類型
無論隱式和顯式的方式,都不推薦使用 any 類型。定義了 any 的參數(shù)會讓使用該組件的人產(chǎn)生極度困惑,無法明確地知道其中的類型。我們可以通過泛型的方式去聲明。
- // 隱式 any (禁止)
- let foo;
- function bar(param) {}
- // 顯式 any (禁止)
- let hello: any;
- function world(param: any) {}
- // 使用泛型繼承,縮小類型范圍 (推薦)
- function Tom<P extends Record<string, any>>(param: P) {}
一個(gè)組件對應(yīng)一個(gè)樣式文件
我們以組件的顆粒度大小為抽象單元,樣式文件則應(yīng)與組件本身保持一致。不推薦交叉引入樣式文件的做法,這樣會導(dǎo)致重構(gòu)混亂,無法明確當(dāng)前這個(gè)樣式被多少個(gè)組件使用。
- - Tom.tsx
- - Tom.module.less
- - Kitty.tsx
- - Kitty.module.less
內(nèi)聯(lián)樣式
避免偷懶,要時(shí)刻保持優(yōu)雅,隨手一個(gè) style={} 是極為不推薦的。這樣不僅每次渲染都有重新創(chuàng)建的消耗,而且是清晰的 JSX 上的噪點(diǎn),影響閱讀。
組件行數(shù)限制
組件需要明確的注釋,并保持 300 行以內(nèi)的代碼行數(shù)。代碼行數(shù)可以通過配置 eslint 來做到限制(可以跳過注釋/空行的的統(tǒng)計(jì)):
- 'max-lines-per-function': [2, { max: 320, skipComments: true, skipBlankLines: true }],
組件內(nèi)部編寫代碼的順序
組件內(nèi)部的順序?yàn)?state -> custom Hooks -> effects -> 內(nèi)部 function -> 其他邏輯 -> JSX
- /**
- * 組件注釋(簡明概要)
- */
- const Kitty: React.FC<KittyProps> = ({ email }) => {
- // 1. state
- // 2. custom Hooks
- // 3. effects
- // 4. 內(nèi)部 function
- // 5. 其他邏輯...
- return (
- <div className={styles.wrapper}>
- {email}
- <Child />
- </div>
- );
- };
事件函數(shù)命名區(qū)分
內(nèi)部方法按照 handle{Type}{Event} 命名,例如 handleNameChange。暴露外部的方法按照 on{Type}{Event},例如 onNameChange。這樣做的好處可以直接通過函數(shù)名區(qū)分是否為外部參數(shù)。
例如 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。某些特殊元素也會擴(kuò)展自己的屬性,例如 InputHTMLAttributes。
我們定義一個(gè)自定義組件則可以通過繼承 React.InputHTMLAttributes
- 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} />;
- }
避免循環(huán)依賴
如果你寫的組件包含了循環(huán)依賴, 這時(shí)候你需要考慮拆分和設(shè)計(jì)模塊文件
- // --- 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 組件就形成了一個(gè)簡單循環(huán)依賴, 盡管它不會造成什么運(yùn)行時(shí)問題. 解決方案就是將 FooProps 抽取到單獨(dú)的文件:
- // --- 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';
相對路徑不要超過兩級
當(dāng)項(xiàng)目復(fù)雜的情況下,目錄結(jié)構(gòu)會越來越深,文件會有很長的 ../ 路徑,這樣看起來很不優(yōu)雅:
- import { ButtonProps } from'../../../components/Button';
我們可以通過在 tsconfig.json 中配置
- "paths": {
- "@/*": ["src/*"]
- }
和 vite 中配置
- alias: {
- '@/': `${path.resolve(process.cwd(), 'src')}/`,
- }
現(xiàn)在我們可以導(dǎo)入相對于 src 的模塊:
- import { ButtonProps } from'@/components/Button';
當(dāng)然更徹底一點(diǎn),可以使用 monorepo 的項(xiàng)目管理方式來解耦各個(gè)組件。只要搭建一套腳手架,就能管理(構(gòu)建、測試、發(fā)布)多個(gè) package
不要直接使用 export default 導(dǎo)出未命名的組件
這種方式導(dǎo)出的組件在 React Inspector 查看時(shí)會顯示為 Unknown
- // 錯(cuò)誤做法
- exportdefault () => {};
- // 正確做法
- exportdefaultfunction Kitty() {}
- // 正確做法:先聲明后導(dǎo)出
- function Kitty() {}
- exportdefault Kitty;
結(jié)語
以上是寫 React 組件在目錄結(jié)構(gòu)以及編碼規(guī)則上需要注意的點(diǎn),后續(xù)我們講解如何在思維上保持優(yōu)雅。