Typescript配合React實踐

blank

Typescript配合React實踐

文章首發: Typescript配合React實踐

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

使用ts的心態變化

剛開始覺得ts好垃圾,覺得React的PropTypedefaultProps幾乎能做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要放在組件的內部
  • 超過兩個地方可以用的東西,要抽象

參考

What do you think?

Written by marketer

blank

哈哈,第二屆VueConf 來啦!

blank

JS趣味算法學習- 實現二叉排序樹