Typescript配合React實踐

使用ts寫React代碼寫了將近三個月,從剛開始覺得特別垃圾到現在覺得沒有ts不行的一些實踐以及思考。 如果按部就班的寫React就體會不到使用ts的樂趣,如果多對代碼進行優化,進行重構,在業務中實踐比較好的一些方案就會體會到ts真正的樂趣,但是ts也在過程中給我帶來了痛苦,在本文的最后會具體展開一下。

使用ts的心態變化

剛開始覺得ts好垃圾,覺得React的PropTypePropDefault幾乎能做ts的靜態類型檢查能做到的事情,甚至做的還能比ts做的多。比如說對于組件間設置默認值,ts對于支持的就是不太好。

后來由于一個需求我改變了一點我的想法,當時的想法就是:“你還別說,這個ts還有點用”。這個場景分為兩種情況:

  1. 父組件傳遞子組件的參數名要發生變化,按照以前都是要通過commamd(ctrl) + f的方式去全局搜索并且修改,但是這樣還是如果對于量大話就很不友好(我遇到的就是量大的情況),如果統一替換的話,比如說這個變量叫做user,就有很大概率會包含其他的變量,這樣統一替換就會很尷尬。但是ts的靜態類型檢查就幫你解決了這個問題,對于每一個父組件沒有傳遞的值來說,都會提示錯誤。而且ts的報錯是在編譯時,不是在運行時
  2. 但是如果傳遞的參數名不變,參數值變了的話,ts的靜態類型也會幫你檢查出來,然后開發人員再去做修改。說了這些比較抽象,上個示例代碼比較清晰:
// 父組件
render(): ReactNode {
    const { user, loading, namespaceList, yarnList, workspace } = this.state;
    return (
      <UserDetail
        user={user}
        loading={loading}
        namespaceList={namespaceList}
        yarnList={yarnList}
        workspace={workspace}
        onLoadWorkspace={(params: IParams) => this.onLoadWorkspace(params)}
        onUpdateUser={(field: string, data: IUserBaseInfo) => this.handleUpdateUser(field, data)}
        onToCreateAk={(userId: number) => this.handleToCreateAk(userId)}
        onDelete={(ids: number[]) => this.handleDelete(ids)}
        onUpdateResource={(userId: number, data: IResources) => this.onUpdateResource(userId, data)}
        onAkDelete={(ak: IPermanentKey) => this.handleDeleteAk(ak)}
        onChangeAkStatus={(ak: IPermanentKey, status: string) => this.onChangeAkStatus(ak, status)}
      />
    );
  }

只要注意第一個參數就可以了,這個是實際的業務場景,下面是子組件:

export interface IProps {
  user: IUser | null;
  loading: boolean;
  namespaceList: { namespace: string }[];
  yarnList: IYarn[];
  workspace: {
    list: IWorkspace[] | null,
    total: number,
  } | null;
  onLoadWorkspace: (params: IParams) => void;
  onUpdateUser: (field: string, data: IUserBaseInfo) => void;
  onToCreateAk: (userId: number) => void;
  onDelete: (ids: number[]) => void;
  onUpdateResource: (userId: number, data: IResources) => void;
  onAkDelete: (ak: IPermanentKey) => void;
  onChangeAkStatus: (ak: IPermanentKey, status: string) => void;
}

class UserDetail extends Form<IProps, {}> {
}

看,如果這樣寫的話,就能覆蓋住上面的兩種情況了。

當我硬著頭皮準備去修改同事上千行的React代碼時候,我剛開始猶豫了好長時間,怕趕在上線發版之前搞不完之類的,后來實踐的時候發現意淫的有點多了,有了ts不用關心這么多了呀。大致為父組件給子組件傳遞的值和回調定義好就ok了。這么說可能有點寬泛,好像自己寫一個組件也是這樣的,哈哈。后面會具體的提到怎么使用ts重構的。這個時候對于ts的心態就是:“這個東西是真的厲害”。

經歷了幾次重構自己和重構其他人代碼的時候,我現在對于ts的心態就是:“我可能以后的前端生涯離不開這玩意兒了”。

項目架構

因為在網上能搜到的ts+react的項目還是比較少,真實的實踐也是比較少,都是一些從頭開始配置項目的。文件的目錄結構怎么做比較好還是沒有具體的實踐方案。當然,這種方案還是要根據具體的業務來分析的。在上一篇文章編寫不用redux的React代碼中說明我當前遇到的業務場景。

最終決定把所有的interface都放在公用的schemas目錄然后在具體的業務中進行具體引用。 具體的common的目錄結構如下(schems目錄下面就保存著所有的接口信息):

common
├── component
│   ├── delete-workspace-modal
│   │   ├── delete-workspace-modal.less
│   │   ├── delete-workspace-modal.less.d.ts
│   │   └── index.tsx
│   └── step-complete
│       ├── index.tsx
│       ├── step-complete.less
│       └── step-complete.less.d.ts
├── css
│   └── global.less
├── hoc
│   ├── workspace-detail.tsx
│   └── workspace-list.tsx
├── schemas
│   ├── dialog.ts
│   ├── k8s.ts
│   ├── ldap.ts
│   ├── message.ts
│   ├── params.ts
│   ├── password.ts
│   ├── section.ts
│   ├── table.ts
│   ├── user.ts
│   ├── workspace.ts
│   └── yarn.ts
└── util
    ├── field-value.ts
    ├── format-datetime.ts
    ├── genURL.ts
    ├── getNamespaceList.ts
    ├── getYarnList.ts
    └── validation.ts

在schems目錄下面的文件就類似于通用的靜態類型,和業務相關但并不是和某個模塊進行強綁定,這是因為在每個模塊之間難免會遇到一些交叉。下面是某個具體模塊的靜態類型:

export interface IYarnResource {
  id: number;
  namespace: string;
  user: string;
  queue: string;
}

export interface IYarnStatus {
  name: string;
  error: string;
  maxCapacity: number;
  state: string;
  used: number;
  capacity: number;
}

export interface IYarnEntity extends IYarnResource {
  status: IYarnStatus;
  keytab: string;
}

和模塊強耦合的靜態類型比如說propsstate的靜態類型,都會放在絕體的業務文件中,就比如說下面的這個代碼(簡化后):

import React, { PureComponent, ReactNode, Fragment } from 'react';
import { IComplex } from 'common/schemas/password';
export interface IProps {
  onClose(): void;
  onOK(data: IComplex): void;
  complex: IComplex | null;
}
export interface IState extends IComplex {
}
class PasswordComplex extends PureComponent<IProps, IState> {
    state: IState = {
    leastLength: 6,
    needCapitalLetter: false,
    needLowercaseLetter: false,
    needNumber: false,
    needSpecialCharacter: false,
  };
}

所有的業務靜態類型一般都是不可復用的,一般是通用靜態類型以及某些特殊的靜態類型組合而成的。

state的初始化不一定要放在constructor里面,但是一定要給state指定類型,具體的原因見:Typescript in React: State will not be placed in the constructor will cause an error

具體靜態類型實踐

如果我們安裝了@types/react,在react目錄下的index.d.ts會有react的所有靜態類型定義。

具體組件架構

現在比如寫一個模塊叫用戶管理,里面包含查看用戶詳情查看用戶列表新建用戶等功能。這也就對應這三個路由/users/:id/users/users/create。這也就對應著三個有狀態組件分別為:user-detail-wrapper, user-list-wrapper,user-form-wrappper。有狀態組件里面只是請求或者獲取數據之類的。展示是通過component下面的無狀態組件。可以看看下面的目錄結構:

user
├── component
│   ├── password-complex
│   │   ├── index.tsx
│   │   ├── password-complex.less
│   │   └── password-complex.less.d.ts
│   ├── user-detail
│   │   ├── index.tsx
│   │   ├── user-detail.less
│   │   └── user-detail.less.d.ts
│   ├── user-detail-ak
│   │   ├── index.tsx
│   │   ├── user-detail-ak.less
│   │   └── user-detail-ak.less.d.ts
│   ├── user-detail-base-info
│   │   ├── index.tsx
│   │   ├── user-detail-base-info.less
│   │   └── user-detail-base-info.less.d.ts
│   ├── user-detail-resource
│   │   ├── index.tsx
│   │   ├── user-detail-resource.less
│   │   └── user-detail-resource.less.d.ts
│   ├── user-detail-workspace
│   │   └── index.tsx
│   ├── user-form-dialog
│   │   ├── index.tsx
│   │   ├── user-form-dialog.less
│   │   └── user-form-dialog.less.d.ts
│   └── user-list
│       ├── index.tsx
│       ├── user-list.less
│       └── user-list.less.d.ts
├── user-form-wrapper
│   └── index.tsx
├── user-detail-wrapper
│   └── index.tsx
└── user-list-wrapper
    └── index.tsx

有狀態組件

設置只讀的state

看過網上的好多實踐,為了防止state的不可篡改,都會把state通過下面的方式設置為只是可讀的,這種方式雖然好,但是在我的項目中不會出現,這種錯誤只有React接觸的新人或者以前寫Vue的人會犯的,我的項目中一共兩個人,不會出現在這種問題。

const defaultState = {
  name: string;
}

type IState = Readonly<typeof defaultState>

class User extends Component<{}, IState> {
  readonly state: IState = defaultState;
}

但是上面這種方式只是適合類型為typescript的基本類型,但是如果有自己定義的復雜類型,比如說下面這種:

interface IUser {
  name: string;
  id: number:
 age: number;
  ...
}

interface IState {
  list: IUser[];
  total: number;
}
// default state

const userList: IUser = []
const defaultState = {
  list: userList,
  total: 0,
}

上面這種就不能通過一個單純的空數組就推斷出list的類型是IUser的數組類型,所以要添加無謂一個userList定義。

無狀態組件

無狀態組件也被稱為展示組件,如果一個展示組件沒有內部的state可以被寫為純函數組件。 如果寫的是函數組件,在@types/react中定義了一個類型type SFC<P = {}> = StatelessComponent<P>;。我們寫函數組件的時候,能指定我們的組件為SFC或者StatelessComponent。這個里面已經預定義了children等,所以我們每次就不用指定類型children的類型了。 下面是一個無狀態組件的例子:

import React, { ReactNode, SFC } from 'react';
import style from './step-complete.less';

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC<IProps> = ({ title, description, children }) => {
  return (
    <div className={style.complete}>
      <div className={style.completeTitle}>
        {title}
      </div>
      <div className={style.completeSubTitle}>
        {description}
      </div>
      <div>
        {children}
      </div>
    </div>
  );
};
export default StepComplete;

泛型組件

先看一個組件,這個組件就是展示一個列表。

import React, { Fragment, PureComponent } from 'react';

export interface IProps<T> {
  total: number;
  list: IYarn[];
  title: string;
  cols: IColumn[];
}

class YarnList  extends PureComponent<IProps> {

}

當我們想通用這個組件的時候,但是就是列表的字段不一樣,也就是列表對應的不同的類型。這個時候我們可是使用泛型,把類型傳遞進來(也可以說是通過typescript的類型推斷來推斷出來)。來看下面的具體實現:

export interface IProps<T> {
  total: number;
  list: T[];
  title: string;
  cols: IColumn[];
}

class ResourceList<T> extends PureComponent<IProps<T>> {
  // 我們現在業務的場景會把這個list傳遞給table,table不同的字段通過外部的父組件傳遞進來。
tableProps(): ITable<T> {
    const columns: IColumn[] = [
      ...this.props.cols,
      { title: '操作', key: 'operation', render: (record: T) => this.renderOperation(record) },
    ];
    return {
      columns,
      data: this.props.list,
      selectable: false,
      enabaleDefaultOperationCol: false,
      searchEmptyText: '沒有搜索到符合條件的資源',
      emptyText: '尚未添加資源',
    };
  }
}

設置默認值

如果使用的typescript是3.x的版本的話,就不用擔心這個問題,就直接在jsx中使用defaultProps就可以了。

如果使用的是2.x的版本就要如果定義一個類似下面這樣一個可選的值:

interface IProps {
  name?: string;
}

我們如果在class里面設置defaultProps的話,ts是不認識的。還是要在代碼里面進行非空判斷。對用這好昂方法可以寫一個高階組件。高階組件來源

export const withDefaultProps = <
  P extends object,
  DP extends Partial<P> = Partial<P>
>(
  defaultProps: DP,
  Cmp: ComponentType<P>,
) => {
  // 提取出必須的屬性
  type RequiredProps = Omit<P, keyof DP>;
  // 重新創建我們的屬性定義,通過一個相交類型,將所有的原始屬性標記成可選的,必選的屬性標記成可選的
  type Props = Partial<DP> & Required<RequiredProps>;

  Cmp.defaultProps = defaultProps;

  // 返回重新的定義的屬性類型組件,通過將原始組件的類型檢查關閉,然后再設置正確的屬性類型
  return (Cmp as ComponentType<any>) as ComponentType<Props>;
};

Typescript不好的地方

就類型定義起來有點費勁,有的時候廢了大半天的力氣發現都是在整ts類型的問題。 然后。。。應該沒有了。

前端開發規范

這里就主要介紹在書寫組件的時候的個人開發規范:

  • 字段內容要盡量到末尾再去解釋。
    • 例: 一個組件要給一個子(子...)傳遞一個對象參數,但是現在可以想象到的這個組件只用name字段,為了可擴展,不要只是給這個子(子...)只是傳遞name屬性,要把整個對象傳遞過去。
    • 例:一個無狀態組件能修改用戶的姓名,當點擊確定按鈕進行修改的時候,不要只是把修改后的姓名傳遞回去,要把整個都傳遞回去。
  • 有狀態組件只是處理響應和請求邏輯,不處理任何展示信息。也就是說有狀態組件中的render函數中只是給子組件傳遞信息
  • 無狀態組件可以保存一些state的信息,比如說一個彈窗的展示和隱藏。
  • 一個組件不能超過300行代碼
  • 兩行鎖進(不同的編譯器使用.editorconfig)
  • 通用的interface放在common下面的schemas下面
  • 非通用的interface比如說IProps或者IState要放在組件的內部
  • 超過兩個地方可以用的東西,要抽象
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容