近兩年來一直在關注 React 開發,最近也開始全面應用 TypeScript 。國內有不少講解 React 和 TypeScript 的教程,但如何將 TypeScript 更好地應用到 React 組件開發模式的文章卻幾乎沒有(也多是我沒找到),特別是 TS 的一些新特性,如:條件類型、條件類型中的類型引用等。這些新特性如何應用到 React 組件開發?沒辦法只能去翻一些國外的文章,結合 TS 的官方文檔慢慢摸索... 因而就有了想法把這個過程整理成文檔。html
本文內容很長,但願你有個舒服的椅子,咱們立刻開始。react
全部代碼均使用 React 16.三、TypeScript 2.9 + strict mode 編寫
所有示例代碼都在這裏
本文假設你已經對 React、TypeScript 有必定的瞭解。我不會講到例如:webpack 打包、Babel 轉碼、TypeScript 編譯選項這一類的問題,而將一切焦點放在如何將 TS 2.8+ 更好地應用到 React 組件設計模式中。webpack
首先,咱們從無狀態組件開始。git
無狀態組件就是沒有 state
的,一般咱們也叫作純函數組件。用原生 JS 咱們能夠這樣寫一個按鈕組件:github
import React from 'react'; const Button = ({onClick: handleClick, children}) => ( <button onClick={handleClick}>{children}</button> );
若是你把代碼直接放到 .tsx
文件中,tsc
編譯器立刻會提示錯誤:有隱含的 any 類型,由於用了嚴格模式。咱們必須明確的定義組件屬性,修改一下:web
import React, { MouseEvent, ReactNode } from 'react'; interface Props { onClick(e: MouseEvent<HTMLElement>): void; children?: ReactNode; }; const Button = ({ onClick: handleClick, children }: Props) => ( <button onClick={handleClick}>{children}</button> );
OK,錯誤沒有了!好像已經完事了?其實再花點心思能夠作的更好。typescript
React 中有個預約義的類型,SFC
:設計模式
type SFC<P = {}> = StatelessComponent<P>;
他是 StatelessComponent
的一個別名,而 StatelessComponent
聲明瞭純函數組件的一些預約義示例屬性和靜態屬性,如:children
、defaultProps
、displayName
等,因此咱們不須要本身寫全部的東西!數組
最後咱們的代碼是這樣的:安全
接着咱們來建立一個計數器按鈕組件。首先咱們定義初始狀態:
const initialState = {count: 0};
而後,定義一個別名 State
並用 TS 推斷出類型:
type State = Readonly<typeof initialState>;
知識點:這樣作不用分開維護接口聲明和實現代碼,比較實用的技巧
同時應該注意到,咱們將全部的狀態屬性聲明爲 readonly
。而後咱們須要明肯定義 state 爲組件的實例屬性:
readonly state: State = initialState;
爲何要這樣作?咱們知道在 React 中咱們不能直接改變 State
的屬性值或者 State
自己:
this.state.count = 1; this.state = {count: 1};
若是這樣作在運行時將會拋出錯誤,但在編寫代碼時卻不會。因此咱們須要明確的聲明 readonly
,這樣 TS 會讓咱們知道若是執行了這種操做就會出錯了:
下面是完整的代碼:
這個組件不須要外部傳遞任何
Props
,因此泛型的第一個參數給的是不帶任何屬性的對象
讓咱們來擴展一下純函數按鈕組件,加上一個顏色屬性:
interface Props { onClick(e: MouseEvent<HTMLElement>): void; color: string; }
若是想要定義屬性默認值的話,咱們知道能夠經過 Button.defaultProps = {...}
作到。而且咱們須要把這個屬性聲明爲可選屬性:(注意屬性名後的 ?
)
interface Props { onClick(e: MouseEvent<HTMLElement>): void; color?: string; }
那麼組件如今看起來是這樣的:
const Button: SFC<Props> = ({onClick: handleClick, color, children}) => ( <button style={{color}} onClick={handleClick}>{children}</button> );
一切看起來好像都很簡單,可是這裏有一個「痛點」。注意咱們使用了 TS 的嚴格模式,color?: string
這個可選屬性的類型如今是聯合類型 -- string | undefined
。
這意味着什麼?若是你要對這種屬性進行一些操做,好比:substr()
,TS 編譯器會直接報錯,由於類型有多是 undefined
,TS 並不知道屬性默認值會由 Component.defaultProps
來建立。
碰到這種狀況咱們通常用兩種方式來解決:
!
後綴,像這樣:color!.substr(...)
。if (color) ...
。以上的方式雖然能夠工做但有種畫蛇添足的感受,畢竟默認值已經有了只是 TS 編譯器「不知道」而已。下面來講一種可重用的方案:咱們寫一個 withDefaultProps
函數,利用 TS 2.8 的條件類型映射,能夠很簡單的完成:
這裏涉及到兩個 type 定義,寫在 src/types/global.d.ts
文件裏面:
declare type DiffPropertyNames<T extends string | number | symbol, U> = { [P in T]: P extends U ? never: P }[T]; declare type Omit<T, K> = Pick<T, DiffPropertyNames<keyof T, K>>;
看一下 TS 2.8 的新特性說明 關於 Conditional Types
的說明,就知道這兩個 type
的原理了。
注意 TS 2.9 的新變化:keyof T
的類型是string | number | symbol
的結構子類型。
如今咱們能夠利用 withDefaultProps
函數來寫一個有屬性默認值的組件了:
如今使用這個組件時默認值屬性已經發生做用,是可選的;而且在組件內部使用這些默認值屬性不用再手動斷言了,這些默認值屬性就是必填屬性!感受還不錯對吧
withDefautProps
函數一樣能夠應用在stateful
有狀態的類組件上。
有一種重用組件邏輯的設計方式是:把組件的 children
寫成渲染回調函數或者暴露一個 render
函數屬性出來。咱們將用這種思路來作一個摺疊面板的場景應用。
首先咱們先寫一個 Toggleable
組件,完整的代碼以下:
下面咱們來逐段解釋下這段代碼,首先先看到組件的屬性聲明相關部分:
type Props = Partial<{ children: RenderCallback; render: RenderCallback; }>; type RenderCallback = (args: ToggleableRenderArgs) => React.ReactNode; type ToggleableRenderArgs = { show: boolean; toggle: Toggleable['toggle']; }
咱們須要同時支持 children
或 render
函數屬性,因此這兩個要聲明爲可選的屬性。注意這裏用了 Partial
映射類型,這樣就不須要每一個手動 ?
操做符來聲明可選了。
爲了保持 DRY 原則(Don't repeat yourself ),咱們還聲明瞭 RenderCallback
類型。
最後,咱們將這個回調函數的參數聲明爲一個獨立的類型:ToggleableRenderArgs
。
注意咱們使用了 TS 的查找類型(lookup types ),這樣 toggle
的類型將和類中定義的同名方法類型保持一致:
private toggle = (event: MouseEvent<HTMLElement>) => { this.setState(prevState => ({show: !prevState.show})); };
一樣是爲了 DRY ,TS 很是給力!
接下來是 State 相關的:
const initialState = {show: false}; type State = Readonly<typeof initialState>;
這個沒什麼特別的,跟前面的例子同樣。
剩下的部分就是 渲染回調 設計模式了,代碼很好理解:
class Toggleable extends Component<Props, State> { // ... render() { const {children, render} = this.props; const {show} = this.state; const renderArgs = {show, toggle: this.toggle}; if (render) { return render(renderArgs); } else if (isFunction(children)) { return children(renderArgs); } else { return null; } } // ... }
如今咱們能夠將 children 做爲一個渲染函數傳遞給 Toggleable 組件:
或者將渲染函數傳遞給 render 屬性:
下面咱們來完成摺疊面板剩下的工做,先寫一個 Panel 組件來重用 Toggleable 的邏輯:
最後寫一個 Collapse 組件來完成這個應用:
這裏咱們不談樣式的事情,運行起來看看,跟期待的效果是否一致?
這種方式對於須要擴展渲染內容時很是有用:Toggleable 組件並不知道也不關心具體的渲染內容,但他控制着顯示狀態邏輯!
爲了使組件邏輯更具伸縮性,下面咱們來講說組件注入模式。
那麼什麼是組件注入模式呢?若是你用過 React-Router
,你已經使用過這種模式來定義路由了:
<Route path="/example" component={Example}/>
不一樣於渲染回調模式,咱們使用 component
屬性「注入」一個組件。爲了演示這個模式是如何工做的,咱們將重構摺疊面板這個場景,首先寫一個可重用的 PanelItem 組件:
import { ToggleableComponentProps } from './Toggleable'; type PanelItemProps = { title: string }; const PanelItem: SFC<PanelItemProps & ToggleableComponentProps> = props => { const {title, children, show, toggle} = props; return ( <div onClick={toggle}> <h1>{title}</h1> {show ? children : null} </div> ); };
而後重構 Toggleable 組件:加入新的 component
屬性。對比先頭的代碼,咱們須要作出以下變化:
children
屬性類型更改成 function 或者 ReactNode(當使用 component
屬性時)component
屬性將傳遞一個組件注入進去,這個注入組件的屬性定義上須要有 ToggleableComponentProps
(實際上是原來的 ToggleableRenderArgs
,還記得嗎?) props
屬性,這個屬性將用來傳遞注入組件須要的屬性值。咱們會設置 props
能夠擁有任意的屬性,由於咱們並不知道注入組件會有哪些屬性,固然這樣咱們會丟失 TS 的嚴格類型檢查...const defaultInjectedProps = {props: {} as { [propName: string]: any }}; type DefaultInjectedProps = typeof defaultInjectedProps; type Props = Partial<{ children: RenderCallback | ReactNode; render: RenderCallback; component: ComponentType<ToggleableComponentProps<any>> } & DefaultInjectedProps>;
下一步咱們把原來的 ToggleableRenderArgs
修改成 ToggleableComponentProps
,容許將注入組件須要的屬性經過 <Toggleable props={...}/>
這樣來傳遞:
type ToggleableComponentProps<P extends object = object> = { show: boolean; toggle: Toggleable['toggle']; } & P;
如今咱們還須要重構一下 render
方法:
render() { const {component: InjectedComponent, children, render, props} = this.props; const {show} = this.state; const renderProps = {show, toggle: this.toggle}; if (InjectedComponent) { return ( <InjectedComponent {...props} {...renderProps}> {children} </InjectedComponent> ); } if (render) { return render(renderProps); } else if (isFunction(children)) { return children(renderProps); } else { return null; } }
咱們已經完成了整個 Toggleable 組件的修改,下面是完整的代碼:
最後咱們寫一個 PanelViaInjection
組件來應用組件注入模式:
import React, { SFC } from 'react'; import { Toggleable } from './Toggleable'; import { PanelItemProps, PanelItem } from './PanelItem'; const PanelViaInjection: SFC<PanelItemProps> = ({title, children}) => ( <Toggleable component={PanelItem} props={{title}}> {children} </Toggleable> );
注意:props
屬性沒有類型安全檢查,由於他被定義爲了包含任意屬性的可索引類型:
{ [propName: string]: any }
如今咱們能夠利用這種方式來重現摺疊面板場景了:
class Collapse extends Component { render() { return ( <div> <PanelViaInjection title="標題一"><p>內容1</p></PanelViaInjection> <PanelViaInjection title="標題二"><p>內容2</p></PanelViaInjection> <PanelViaInjection title="標題三"><p>內容3</p></PanelViaInjection> </div> ); } }
在組件注入模式的例子中,props
屬性丟失了類型安全檢查,咱們如何去修復這個問題呢?估計你已經猜出來了,咱們能夠把 Toggleable 組件重構爲泛型組件!
下來咱們開始重構 Toggleable 組件。首先咱們須要讓 props
支持泛型:
type DefaultInjectedProps<P extends object = object> = { props: P }; const defaultInjectedProps: DefaultInjectedProps = {props: {}}; type Props<P extends object = object> = Partial<{ children: RenderCallback | ReactNode; render: RenderCallback; component: ComponentType<ToggleableComponentProps<P>> } & DefaultInjectedProps<P>>;
而後讓 Toggleable 的 class 也支持泛型:
class Toggleable<T extends object = object> extends Component<Props<T>, State> {}
看起來好像已經搞定了!若是你是用的 TS 2.9,能夠直接這樣用:
const PanelViaInjection: SFC<PanelItemProps> = ({title, children}) => ( <Toggleable<PanelItemProps> component={PanelItem} props={{title}}> {children} </Toggleable> );
可是若是 <= TS 2.8 ... JSX 裏面不能直接應用泛型參數 那麼咱們還有一步工做要作,加入一個靜態方法 ofType
,用來進行構造函數的類型轉換:
static ofType<T extends object>() { return Toggleable as Constructor<Toggleable<T>>; }
這裏用到一個 type:Constructor
,依然定義在 src/types/global.d.ts
裏面:
declare type Constructor<T = {}> = { new(...args: any[]): T };
好了,咱們完成了全部的工做,下面是 Toggleable 重構後的完整代碼:
如今咱們來看看怎麼使用這個泛型組件,重構下原來的 PanelViaInjection 組件:
import React, { SFC } from 'react'; import { Toggleable } from './Toggleable'; import { PanelItemProps, PanelItem } from './PanelItem'; const ToggleableOfPanelItem = Toggleable.ofType<PanelItemProps>(); const PanelViaInjection: SFC<PanelItemProps> = ({title, children}) => ( <ToggleableOfPanelItem component={PanelItem} props={{title}}> {children} </ToggleableOfPanelItem> );
全部的功能都能像原來的代碼同樣工做,而且如今 props
屬性也支持 TS 類型檢查了,很棒有木有!
最後咱們來看下 HOC 。前面咱們已經實現了 Toggleable 的渲染回調模式,那麼很天然的咱們能夠衍生出一個 HOC 組件。
若是對 HOC 不熟悉的話,能夠先看下 React 官方文檔對於 HOC 的說明。
先來看看定義 HOC 咱們須要作哪些工做:
displayName
(方便在 devtools 裏面進行調試)WrappedComponent
(能夠訪問原始的組件 -- 有利於調試)下面直接上代碼 -- withToggleable
高階組件:
如今咱們來用 HOC 重寫一個 Panel :
import { PanelItem } from './PanelItem'; import withToggleable from './withToggleable'; const PanelViaHOC = withToggleable(PanelItem);
而後,又能夠實現摺疊面板了
class Collapse extends Component { render() { return ( <div> <PanelViaHOC title="標題一"><p>內容1</p></PanelViaHOC> <PanelViaHOC title="標題二"><p>內容2</p></PanelViaHOC> </div> ); } }
感謝能堅持看完的朋友,大家真的很棒!
全部的示例代碼都在 這裏 ,若是以爲還不錯幫忙給個 star
最後,感謝 Anders Hejlsberg 和全部的 TS 貢獻者