文章首發:Typescript配合React實踐css
使用ts寫React代碼寫了將近三個月,從剛開始以爲特別垃圾到如今以爲沒有ts不行的一些實踐以及思考。 若是循序漸進的寫React就體會不到使用ts的樂趣,若是多對代碼進行優化,進行重構,在業務中實踐比較好的一些方案就會體會到ts真正的樂趣,可是ts也在過程當中給我帶來了痛苦,在本文的最後會具體展開一下。html
剛開始以爲ts好垃圾,以爲React的PropType
和PropDefault
幾乎能作ts的靜態類型檢查能作到的事情,甚至作的還能比ts作的多。好比說對於組件間設置默認值,ts對於支持的就是不太好。前端
後來因爲一個需求我改變了一點個人想法,當時的想法就是:「你還別說,這個ts還有點用」。這個場景分爲兩種狀況:react
commamd(ctrl) + f
的方式去全局搜索而且修改,可是這樣仍是若是對於量大話就很不友好(我遇到的就是量大的狀況),若是統一替換的話,好比說這個變量叫作user,就有很大機率會包含其餘的變量,這樣統一替換就會很尷尬。可是ts的靜態類型檢查就幫你解決了這個問題,對於每個父組件沒有傳遞的值來講,都會提示錯誤。並且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)}
/>
);
}
複製代碼
只要注意第一個參數就能夠了,這個是實際的業務場景,下面是子組件:ios
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, {}> {
複製代碼
看,若是這樣寫的話,就能覆蓋住上面的兩種狀況了。git
當我硬着頭皮準備去修改同事上千行的React代碼時候,我剛開始猶豫了好長時間,怕趕在上線發版以前搞不完之類的,後來實踐的時候發現意淫的有點多了,有了ts不用關心這麼多了呀。大體爲父組件給子組件傳遞的值和回調定義好就ok了。這麼說可能有點寬泛,好像本身寫一個組件也是這樣的,哈哈。後面會具體的提到怎麼使用ts重構的。這個時候對於ts的心態就是:「這個東西是真的厲害」。github
經歷了幾回重構本身和重構其餘人代碼的時候,我如今對於ts的心態就是:「我可能之後的前端生涯離不開這玩意兒了」。typescript
由於在網上能搜到的ts+react
的項目仍是比較少,真實的實踐也是比較少,都是一些從頭開始配置項目的。文件的目錄結構怎麼作比較好仍是沒有具體的實踐方案。固然,這種方案仍是要根據具體的業務來分析的。在上一篇文章編寫不用redux的React代碼中說明我當前遇到的業務場景。redux
最終決定把全部的interface都放在公用的schemas目錄而後在具體的業務中進行具體引用。 具體的common的目錄結構以下(schems目錄下面就保存着全部的接口信息):api
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;
}
複製代碼
和模塊強耦合的靜態類型好比說props和state的靜態類型,都會放在絕體的業務文件中,就好比說下面的這個代碼(簡化後):
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
經過下面的方式設置爲只是可讀的,這種方式雖然好,可是在個人項目中不會出現,這種錯誤只有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>;
};
複製代碼
就類型定義起來有點費勁,有的時候廢了大半天的力氣發現都是在整ts類型的問題。 而後。。。應該沒有了。
這裏就主要介紹在書寫組件的時候的我的開發規範:
render
函數中只是給子組件傳遞信息interface
放在common下面的schemas下面interface
好比說IProps
或者IState
要放在組件的內部