摘要:近兩年來一直在關(guān)注開發(fā),最近也開始全面應(yīng)用。首先,我們從無狀態(tài)組件開始。渲染回調(diào)模式有一種重用組件邏輯的設(shè)計方式是把組件的寫成渲染回調(diào)函數(shù)或者暴露一個函數(shù)屬性出來。最后,我們將這個回調(diào)函數(shù)的參數(shù)聲明為一個獨立的類型。
近兩年來一直在關(guān)注 React 開發(fā),最近也開始全面應(yīng)用 TypeScript 。國內(nèi)有很多講解 React 和 TypeScript 的教程,但如何將 TypeScript 更好地應(yīng)用到 React 組件開發(fā)模式的文章卻幾乎沒有(也可能是我沒找到),特別是 TS 的一些新特性,如:條件類型、條件類型中的類型引用等。這些新特性如何應(yīng)用到 React 組件開發(fā)?沒辦法只能去翻一些國外的文章,結(jié)合 TS 的官方文檔慢慢摸索... 于是就有了想法把這個過程整理成文檔。
本文內(nèi)容很長,希望你有個舒服的椅子,我們馬上開始。
所有代碼均使用 React 16.3、TypeScript 2.9 + strict mode 編寫開始
全部示例代碼都在這里
本文假設(shè)你已經(jīng)對 React、TypeScript 有一定的了解。我不會講到例如:webpack 打包、Babel 轉(zhuǎn)碼、TypeScript 編譯選項這一類的問題,而將一切焦點放在如何將 TS 2.8+ 更好地應(yīng)用到 React 組件設(shè)計模式中。
首先,我們從無狀態(tài)組件開始。
無狀態(tài)組件無狀態(tài)組件就是沒有 state 的,通常我們也叫做純函數(shù)組件。用原生 JS 我們可以這樣寫一個按鈕組件:
import React from "react"; const Button = ({onClick: handleClick, children}) => ( );
如果你把代碼直接放到 .tsx 文件中,tsc 編譯器馬上會提示錯誤:有隱含的 any 類型,因為用了嚴(yán)格模式。我們必須明確的定義組件屬性,修改一下:
import React, { MouseEvent, ReactNode } from "react"; interface Props { onClick(e: MouseEvent): void; children?: ReactNode; }; const Button = ({ onClick: handleClick, children }: Props) => ( );
OK,錯誤沒有了!好像已經(jīng)完事了?其實再花點心思可以做的更好。
React 中有個預(yù)定義的類型,SFC :
type SFC= StatelessComponent
;
他是 StatelessComponent 的一個別名,而 StatelessComponent 聲明了純函數(shù)組件的一些預(yù)定義示例屬性和靜態(tài)屬性,如:children、defaultProps、displayName 等,所以我們不需要自己寫所有的東西!
最后我們的代碼是這樣的:
有狀態(tài)的類組件接著我們來創(chuàng)建一個計數(shù)器按鈕組件。首先我們定義初始狀態(tài):
const initialState = {count: 0};
然后,定義一個別名 State 并用 TS 推斷出類型:
type State = Readonly;
知識點:這樣做不用分開維護接口聲明和實現(xiàn)代碼,比較實用的技巧
同時應(yīng)該注意到,我們將所有的狀態(tài)屬性聲明為 readonly 。然后我們需要明確定義 state 為組件的實例屬性:
readonly state: State = initialState;
為什么要這樣做?我們知道在 React 中我們不能直接改變 State 的屬性值或者 State 本身:
this.state.count = 1; this.state = {count: 1};
如果這樣做在運行時將會拋出錯誤,但在編寫代碼時卻不會。所以我們需要明確的聲明 readonly ,這樣 TS 會讓我們知道如果執(zhí)行了這種操作就會出錯了:
下面是完整的代碼:
這個組件不需要外部傳遞任何 Props ,所以泛型的第一個參數(shù)給的是不帶任何屬性的對象屬性默認(rèn)值
讓我們來擴展一下純函數(shù)按鈕組件,加上一個顏色屬性:
interface Props { onClick(e: MouseEvent): void; color: string; }
如果想要定義屬性默認(rèn)值的話,我們知道可以通過 Button.defaultProps = {...} 做到。并且我們需要把這個屬性聲明為可選屬性:(注意屬性名后的 ? )
interface Props { onClick(e: MouseEvent): void; color?: string; }
那么組件現(xiàn)在看起來是這樣的:
const Button: SFC= ({onClick: handleClick, color, children}) => ( );
一切看起來好像都很簡單,但是這里有一個“痛點”。注意我們使用了 TS 的嚴(yán)格模式,color?: string 這個可選屬性的類型現(xiàn)在是聯(lián)合類型 -- string | undefined 。
這意味著什么?如果你要對這種屬性進行一些操作,比如:substr() ,TS 編譯器會直接報錯,因為類型有可能是 undefined ,TS 并不知道屬性默認(rèn)值會由 Component.defaultProps 來創(chuàng)建。
碰到這種情況我們一般用兩種方式來解決:
使用類型斷言手動去除,添加 ! 后綴,像這樣:color!.substr(...) 。
使用條件判斷或者三元操作符讓 TS 編譯器知道這個屬性不是 undefined,比如: if (color) ... 。
以上的方式雖然可以工作但有種多此一舉的感覺,畢竟默認(rèn)值已經(jīng)有了只是 TS 編譯器“不知道”而已。下面來說一種可重用的方案:我們寫一個 withDefaultProps 函數(shù),利用 TS 2.8 的條件類型映射,可以很簡單的完成:
這里涉及到兩個 type 定義,寫在 src/types/global.d.ts 文件里面:
declare type DiffPropertyNames= { [P in T]: P extends U ? never: P }[T]; declare type Omit = Pick >;
看一下 TS 2.8 的新特性說明 關(guān)于 Conditional Types 的說明,就知道這兩個 type 的原理了。
注意 TS 2.9 的新變化:keyof T 的類型是 string | number | symbol 的結(jié)構(gòu)子類型。
現(xiàn)在我們可以利用 withDefaultProps 函數(shù)來寫一個有屬性默認(rèn)值的組件了:
現(xiàn)在使用這個組件時默認(rèn)值屬性已經(jīng)發(fā)生作用,是可選的;并且在組件內(nèi)部使用這些默認(rèn)值屬性不用再手動斷言了,這些默認(rèn)值屬性就是必填屬性!感覺還不錯對吧
withDefautProps 函數(shù)同樣可以應(yīng)用在 stateful 有狀態(tài)的類組件上。渲染回調(diào)模式
有一種重用組件邏輯的設(shè)計方式是:把組件的 children 寫成渲染回調(diào)函數(shù)或者暴露一個 render 函數(shù)屬性出來。我們將用這種思路來做一個折疊面板的場景應(yīng)用。
首先我們先寫一個 Toggleable 組件,完整的代碼如下:
下面我們來逐段解釋下這段代碼,首先先看到組件的屬性聲明相關(guān)部分:
type Props = Partial<{ children: RenderCallback; render: RenderCallback; }>; type RenderCallback = (args: ToggleableRenderArgs) => React.ReactNode; type ToggleableRenderArgs = { show: boolean; toggle: Toggleable["toggle"]; }
我們需要同時支持 children 或 render 函數(shù)屬性,所以這兩個要聲明為可選的屬性。注意這里用了 Partial 映射類型,這樣就不需要每個手動 ? 操作符來聲明可選了。
為了保持 DRY 原則(Don"t repeat yourself?),我們還聲明了 RenderCallback 類型。
最后,我們將這個回調(diào)函數(shù)的參數(shù)聲明為一個獨立的類型:ToggleableRenderArgs 。
注意我們使用了 TS 的查找類型(lookup types ),這樣 toggle 的類型將和類中定義的同名方法類型保持一致:
private toggle = (event: MouseEvent) => { this.setState(prevState => ({show: !prevState.show})); };
同樣是為了 DRY ,TS 非常給力!
接下來是 State 相關(guān)的:
const initialState = {show: false}; type State = Readonly;
這個沒什么特別的,跟前面的例子一樣。
剩下的部分就是 渲染回調(diào) 設(shè)計模式了,代碼很好理解:
class Toggleable extends Component{ // ... 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; } } // ... }
現(xiàn)在我們可以將 children 作為一個渲染函數(shù)傳遞給 Toggleable 組件:
或者將渲染函數(shù)傳遞給 render 屬性:
下面我們來完成折疊面板剩下的工作,先寫一個 Panel 組件來重用 Toggleable 的邏輯:
最后寫一個 Collapse 組件來完成這個應(yīng)用:
這里我們不談樣式的事情,運行起來看看,跟期待的效果是否一致?
這種方式對于需要擴展渲染內(nèi)容時非常有用:Toggleable 組件并不知道也不關(guān)心具體的渲染內(nèi)容,但他控制著顯示狀態(tài)邏輯!組件注入模式
為了使組件邏輯更具伸縮性,下面我們來說說組件注入模式。
那么什么是組件注入模式呢?如果你用過 React-Router ,你已經(jīng)使用過這種模式來定義路由了:
不同于渲染回調(diào)模式,我們使用 component 屬性“注入”一個組件。為了演示這個模式是如何工作的,我們將重構(gòu)折疊面板這個場景,首先寫一個可重用的 PanelItem 組件:
import { ToggleableComponentProps } from "./Toggleable"; type PanelItemProps = { title: string }; const PanelItem: SFC= props => { const {title, children, show, toggle} = props; return ( ); };{title}
{show ? children : null}
然后重構(gòu) Toggleable 組件:加入新的 component 屬性。對比先頭的代碼,我們需要做出如下變化:
children 屬性類型更改為 function 或者 ReactNode(當(dāng)使用 component 屬性時)
component 屬性將傳遞一個組件注入進去,這個注入組件的屬性定義上需要有 ToggleableComponentProps (其實是原來的 ToggleableRenderArgs ,還記得嗎?)
還需要定義一個 props 屬性,這個屬性將用來傳遞注入組件需要的屬性值。我們會設(shè)置 props 可以擁有任意的屬性,因為我們并不知道注入組件會有哪些屬性,當(dāng)然這樣我們會丟失 TS 的嚴(yán)格類型檢查...
const defaultInjectedProps = {props: {} as { [propName: string]: any }}; type DefaultInjectedProps = typeof defaultInjectedProps; type Props = Partial<{ children: RenderCallback | ReactNode; render: RenderCallback; component: ComponentType> } & DefaultInjectedProps>;
下一步我們把原來的 ToggleableRenderArgs 修改為 ToggleableComponentProps ,允許將注入組件需要的屬性通過
type ToggleableComponentProps= { show: boolean; toggle: Toggleable["toggle"]; } & P;
現(xiàn)在我們還需要重構(gòu)一下 render 方法:
render() { const {component: InjectedComponent, children, render, props} = this.props; const {show} = this.state; const renderProps = {show, toggle: this.toggle}; if (InjectedComponent) { return ({children} ); } if (render) { return render(renderProps); } else if (isFunction(children)) { return children(renderProps); } else { return null; } }
我們已經(jīng)完成了整個 Toggleable 組件的修改,下面是完整的代碼:
最后我們寫一個 PanelViaInjection 組件來應(yīng)用組件注入模式:
import React, { SFC } from "react"; import { Toggleable } from "./Toggleable"; import { PanelItemProps, PanelItem } from "./PanelItem"; const PanelViaInjection: SFC= ({title, children}) => ( {children} );
注意:props 屬性沒有類型安全檢查,因為他被定義為了包含任意屬性的可索引類型:
{ [propName: string]: any }
現(xiàn)在我們可以利用這種方式來重現(xiàn)折疊面板場景了:
class Collapse extends Component { render() { return (泛型組件); } }內(nèi)容1
內(nèi)容2
內(nèi)容3
在組件注入模式的例子中,props 屬性丟失了類型安全檢查,我們?nèi)绾稳バ迯?fù)這個問題呢?估計你已經(jīng)猜出來了,我們可以把 Toggleable 組件重構(gòu)為泛型組件!
下來我們開始重構(gòu) Toggleable 組件。首先我們需要讓 props 支持泛型:
type DefaultInjectedProps= { props: P }; const defaultInjectedProps: DefaultInjectedProps = {props: {}}; type Props
= Partial<{ children: RenderCallback | ReactNode; render: RenderCallback; component: ComponentType
> } & DefaultInjectedProps >;
然后讓 Toggleable 的 class 也支持泛型:
class Toggleableextends Component , State> {}
看起來好像已經(jīng)搞定了!如果你是用的 TS 2.9,可以直接這樣用:
const PanelViaInjection: SFC= ({title, children}) => ( component={PanelItem} props={{title}}> {children} );
但是如果 <= TS 2.8 ... JSX 里面不能直接應(yīng)用泛型參數(shù) 那么我們還有一步工作要做,加入一個靜態(tài)方法 ofType ,用來進行構(gòu)造函數(shù)的類型轉(zhuǎn)換:
static ofType() { return Toggleable as Constructor >; }
這里用到一個 type:Constructor,依然定義在 src/types/global.d.ts 里面:
declare type Constructor= { new(...args: any[]): T };
好了,我們完成了所有的工作,下面是 Toggleable 重構(gòu)后的完整代碼:
現(xiàn)在我們來看看怎么使用這個泛型組件,重構(gòu)下原來的 PanelViaInjection 組件:
import React, { SFC } from "react"; import { Toggleable } from "./Toggleable"; import { PanelItemProps, PanelItem } from "./PanelItem"; const ToggleableOfPanelItem = Toggleable.ofType(); const PanelViaInjection: SFC = ({title, children}) => ( {children} );
所有的功能都能像原來的代碼一樣工作,并且現(xiàn)在 props 屬性也支持 TS 類型檢查了,很棒有木有!
高階組件最后我們來看下 HOC 。前面我們已經(jīng)實現(xiàn)了 Toggleable 的渲染回調(diào)模式,那么很自然的我們可以衍生出一個 HOC 組件。
如果對 HOC 不熟悉的話,可以先看下 React 官方文檔對于 HOC 的說明。
先來看看定義 HOC 我們需要做哪些工作:
displayName (方便在 devtools 里面進行調(diào)試)
WrappedComponent (可以訪問原始的組件 -- 有利于調(diào)試)
引入 hoist-non-react-statics 包,將原始組件的靜態(tài)方法全部“復(fù)制”到 HOC 組件上
下面直接上代碼 -- withToggleable 高階組件:
現(xiàn)在我們來用 HOC 重寫一個 Panel :
import { PanelItem } from "./PanelItem"; import withToggleable from "./withToggleable"; const PanelViaHOC = withToggleable(PanelItem);
然后,又可以實現(xiàn)折疊面板了
class Collapse extends Component { render() { return (尾聲); } }內(nèi)容1
內(nèi)容2
感謝能堅持看完的朋友,你們真的很棒!
所有的示例代碼都在 這里 ,如果覺得還不錯幫忙給個 star
最后,感謝 Anders?Hejlsberg 和所有的 TS 貢獻者
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/95362.html
摘要:詳情發(fā)布新版本中可以自動修復(fù)和合并沖突的文件,還新增了命令。詳情是一個用構(gòu)建設(shè)計系統(tǒng)的開源工具,提供了一套基礎(chǔ)應(yīng)用程序開發(fā)的工具,模式和實踐。目前,只有和的最新版本支持該屬性。詳情每周一同步更新到歡迎 01. JS 引擎 V8 v6.6 的更新 最新 v6.6 版本的 V8 JavaScript 引擎更新了方法 Function.prototype.toString(),改進了代碼緩存...
摘要:詳情發(fā)布新版本中可以自動修復(fù)和合并沖突的文件,還新增了命令。詳情是一個用構(gòu)建設(shè)計系統(tǒng)的開源工具,提供了一套基礎(chǔ)應(yīng)用程序開發(fā)的工具,模式和實踐。目前,只有和的最新版本支持該屬性。詳情每周一同步更新到歡迎 01. JS 引擎 V8 v6.6 的更新 最新 v6.6 版本的 V8 JavaScript 引擎更新了方法 Function.prototype.toString(),改進了代碼緩存...
摘要:詳情發(fā)布新版本中可以自動修復(fù)和合并沖突的文件,還新增了命令。詳情是一個用構(gòu)建設(shè)計系統(tǒng)的開源工具,提供了一套基礎(chǔ)應(yīng)用程序開發(fā)的工具,模式和實踐。目前,只有和的最新版本支持該屬性。詳情每周一同步更新到歡迎 01. JS 引擎 V8 v6.6 的更新 最新 v6.6 版本的 V8 JavaScript 引擎更新了方法 Function.prototype.toString(),改進了代碼緩存...
摘要:怎么影響了我的思考方式對前端開發(fā)者來說,能強化了面向接口編程這一理念。使用的過程就是在加深理解的過程,確實面向接口編程天然和靜態(tài)類型更為親密。 電影《降臨》中有一個觀點,語言會影響人的思維方式,對于前端工程師來說,使用 typescript 開發(fā)無疑就是在嘗試換一種思維方式做事情。 其實直到最近,我才開始系統(tǒng)的學(xué)習(xí) typescript ,前后大概花了一個月左右的時間。在這之前,我也在...
摘要:怎么影響了我的思考方式對前端開發(fā)者來說,能強化了面向接口編程這一理念。使用的過程就是在加深理解的過程,確實面向接口編程天然和靜態(tài)類型更為親密。摘要: 學(xué)會TS思考方式。 原文:TypeScript - 一種思維方式 作者:zhangwang Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所有。 電影《降臨》中有一個觀點,語言會影響人的思維方式,對于前端工程師來說,使用 typescript 開...
閱讀 3456·2021-09-08 09:36
閱讀 2534·2019-08-30 15:54
閱讀 2345·2019-08-30 15:54
閱讀 1761·2019-08-30 15:44
閱讀 2378·2019-08-26 14:04
閱讀 2437·2019-08-26 14:01
閱讀 2869·2019-08-26 13:58
閱讀 1315·2019-08-26 13:47