typescript + react 專案開發體驗之 react

leekkj發表於2019-01-19

目錄

整體框架的理解

​ 電影是由一個個幀構成,播放的過程就是幀的替換。react 官方文件元素渲染模組有句這樣的話:React 元素都是不可變的。當元素被建立之後,你是無法改變其內容或屬性的。一個元素就好像是動畫裡的一幀,它代表應用介面在某一時間點的樣子。

​ 結合實際去理解,就是當頁面構建,網頁就形成了初始幀,網頁上的展現內容是由state去控制,react可以通過setState通過改變狀態去做幀的切換,由此實現頁面展現效果的變化。為了提升效能,並儘可能快且正確的切換幀,react做了如下優化:

  1. 非同步的setState。
  2. 採用diff演算法,儘可能快的進行元素比較,並找到相應元素替換。
  3. 引用fiber演算法,減少深層級元件對網頁響應的影響。

react中使用ts

  1. 元件定義
//我們可以在 /node_modules/@type/React 找到有關Component的定義,我們擷取部分
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }
class Component<P, S> {
    constructor(props: Readonly<P>);
    state: Readonly<S>;
    // ....
}
// 可以看出定義一個元件可以通過定義泛型去指定他的props和state型別
interface IDemoProps{
    a: string,
    b: string
}
interface IDemoState{
    c: string,
    d: string
}
// 當你定義一個元件時定義了State型別,你至少以下列一種方式指定state屬性,否則會報錯;
class Demo extends Component<IDemoProps, IDemoState>{
	// 方式一
    state={
        c: 1,
        d: 2
    }
    constructor(props: IDemoProps){
        super(props);
        // 方式二
        this.state = {
            c: 'c',
            d: 'd'
        }
        props.c // 報錯,屬性不存在
    }
    render(){
        return <>
            <div>{this.state.c}</div>
            <div>{this.props.a}</div>
        </>
    }
}
// 當元件內部不需要使用生命週期鉤子,或者元件內不儲存自身狀態時,可簡化成函式式元件
// 同樣我們找到定義方式
interface FunctionComponent<P = {}> {
    (props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
	//	...
}
const Demo = (props: IDemoProps)=>{
    return <>
            <div>{this.state.c}</div>
            <div>{this.props.a}</div>
    </>
}
// pureComponent 在寫元件的過程中同學會用到 pureComponent 去提升元件的效能
// 從描述上來看它和普通的Component沒有區別
class PureComponent<P = {}, S = {}, SS = any> extends Component<P, S, SS> { }
// 擷取一段 pureComponent 原始碼進行比較發現他就是當元件不存在shouldComponetUpdate這個鉤子時,會自己新增一條規則即前後 props 和 state 進行淺比較判斷是否需要修改元件。
if (inst.shouldComponentUpdate) {
  shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
} else {
  if (this._compositeType === CompositeType.PureClass) {
    // 用 shallowEqual 對比 props 和 state 的改動
    // 如果都沒改變就不用更新
    shouldUpdate =
      !shallowEqual(prevProps, nextProps) ||
      !shallowEqual(inst.state, nextState);
  }
}

//在 /node_modules/@type/React 中還可以找到這些,我可以理解成這是component的介面宣告方式
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
    new (props: P, context?: any): Component<P, S>;
	// ....
}
// sfc fc StatelessComponent 都是 FunctionComponent 的別名。
type ComponentState = any;
type SFC<P = {}> = FunctionComponent<P>;
type StatelessComponent<P = {}> = FunctionComponent<P>;
type FC<P = {}> = FunctionComponent<P>;
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;
// 因此當我們以元件作為引數傳遞的時候可以使用ComponentType進行宣告
複製程式碼

ps: 當我們用第三方庫的時候,不妨看看他們的d.ts檔案,這個可能是最好的文件。

  1. 檢視的拆分

我們在元件的書寫過程中,由於展示檢視過於複雜,render函式的時候常常會遇到大段大段的dom和一些複雜的條件渲染。如果把他們都放在同一個函式下會造成元件程式碼太長,不利於維護,因此拆分就很有必要了。我常常用到以下幾種拆分方式:

// 拆分成元件,把大元件拆分成多個小元件,這時候不免一些方法的傳遞,為了使得 this 繫結當前元件,定義方法時可以採用下面switchItemHandler這種方式,避免再進行一個bind。
//...component dosomthing....
switchItemHandler = (checkItem:checkedItemData)=>{
    this.setState...
}
render(){
        return <>
        <TabHead
            switchItemHandler={this.switchItemHandler}
            // ...
        ></TabHead>
        <MiddleBar
        	//....
        ></MiddleBar>
        <ScrollEndLoadComponent
            //....
		></ScrollEndLoadComponent>
    </>
 }
// 有時候逐個引數傳遞太麻煩了我們可以定義一個返回jsxElement的函式,然後通過call去呼叫,注意,使用bind、call、apply的函式,在ts中的定義,需要在形參中加入this的型別定義。
export const comfirm = function(this: GoodsBuy){
  return <div className="flex-bottom">
    <div className="button-fill-large" onClick={this.createOrder}>
      確認兌換
    </div>
  </div>
}
// class GoodsBuy
render(){
    return <div className="goodsBuy">
      {goodsAndCount.call(this)}
      {total.call(this)}
      {comfirm.call(this)}
    </div>
}
複製程式碼

一個元件說說HOC 、 props render

在手機端業務中當有長列表,常常需要逐步載入相應的需要展現的內容。頁面滾動至底部載入就是其中一個策略。接下來我們就來講這個功能進行抽離實現元件化。

滾動至底部載入我們可以把這個邏輯進行拆分一下。

  1. 監聽滾動條事件的監聽。
  2. 資料載入策略。
  3. 具體列表內容的展現。

做過微信小程式的同學可能記得它提供一個onReachBottom上拉觸底的鉤子,參照這個設計思路我希望我定義元件時,加入一個鉤子,在滾動到底部的時候這個鉤子會被呼叫。

// component
scrollInBottom = ()=>{
    // do something..
}
複製程式碼

通常情況下我們需要去做監聽呼叫。

scrollBottomHandler = ()=>{
    if(document.body.scrollHeight - document.body.scrollTop  <  innerHeight + 10)
        this.scrollInBottom();
}
componentDidMount(){
    document.addEventListener('scroll',this.scrollBottomHandler)
}
componentWillUnmount(){
    document.removeEventListener('scroll', this.scrollBottomHandler);
}
複製程式碼

我現在想把這個功能抽離,目前有兩種思路:

  1. 定義一個具有著三個方法的類,通過 extends 使得現有Component也能具有這個三個方法。
  2. 定義一個接收一個classComponent(具備一個鉤子scrollInBottom)的函式,返回一個元件,這個元件進行滾動監聽,當滾動到底部的時候呼叫classComponent 的component方法(這也就是我們們常說的HOC)。

第一種方法有個很吃癟的地方,就是當前元件如果定義了這三個方法時,會覆蓋extends的方法,使得功能失效,需要額外的super操作,這裡就不細說了。於是我毅然決然的選擇了第二種方式。

// 我們來根據第二種思路來描述這個方法
// 定義一個具有scrollInBottom:()=>void函式作為屬性的react元件
type IComponet = {scrollInBottom: ()=>void} & Component;
// 定義一個能獲取ref,例項化後能生成具有scrollInBottom的元件。
interface IHasScrollBottomClass<P = {}, S = ComponentState> extends ComponentClass<P, S>{
    new (props: P, context?: any): IComponet
}
// 接下來就是上面思路2的描述了,就不贅述啦。
const scrollBottomLoad = <T extends object,S = {}>(WrapComponent: IHasScrollBottomClass<T, S>)=>{
    return class extends Component<T>{
        subRef: IComponet | null = null
        scrollBottomHandler = ()=>{
            if(!this.subRef)return;
            if(document.body.scrollHeight - document.body.scrollTop  <  innerHeight + 10)
                this.subRef.scrollInBottom();
        }
        componentDidMount(){
            document.addEventListener('scroll',this.scrollBottomHandler)
        }
        componentWillUnmount(){
            document.removeEventListener('scroll', this.scrollBottomHandler);
        }
        render(){
            return <WrapComponent
                ref={cp=> this.subRef = cp}
                {...this.props}
            ></WrapComponent>
        }
    }
}
複製程式碼

至此,滾動事件監聽功能就已經抽離出來了。接下來我們要抽離載入和具體內容展示。我們碼上說話

interface QueryListModel{
  start: number;
  limit: number;
}
// 定義元件接收3個引數
interface ILoadDataAndCheckMoreProps<T, K> {
    loadFuc: (queryCondition: T & QueryListModel)=>Promise<K[]>;//資料載入函式
    queryCondition: T;// 除基礎模型歪的查詢條件
    render: (props: K[])=>ReactElement<{ list: K[]}>; // 渲染列表的方法
}
// 本地相關state分別儲存
interface ILoadDataAndCheckMoreState<T, K>{
    noMore: boolean // 是否還有資料
    queryConditionCombin: T & QueryListModel // 條件列表
    list: K[] // 資料列表
}
const ANYTIME_REQUEST_ITEMNUMBER = 10; // 每次請求他的條數
class LoadDataAndCheckMore<T extends object, K extends object> extends Component<ILoadDataAndCheckMoreProps<T, K>, ILoadDataAndCheckMoreState<T, K>> {
    constructor(props:ILoadDataAndCheckMoreProps<T, K>){
        super(props);
        // 初始化3個狀態
        this.state = {
            noMore: false,
            queryConditionCombin: this.initQueryCondition(props.queryCondition),
            list: []
        }
    }
    // 初始化狀態,為啥這裡要是負數呢?往下看。
    initQueryCondition = (props: T)=>{return Object.assign({}, props, {start: -ANYTIME_REQUEST_ITEMNUMBER, limit: 10 })}
    // 資料載入
    loadMore = ()=>{
        // 如果沒有資料了,不再載入
        if(this.state.noMore)return;
        // 這就是上面為什麼start要為負數
        this.state.queryConditionCombin.start += ANYTIME_REQUEST_ITEMNUMBER;
        // 每次請求之後,並檢查還沒有更多。
        this.loadListAndCheckNoMore().then((data: K[])=>{
            this.setState({
                list: this.state.list.concat(data)
            })
            Toast.hide();
        })
        // loading相關
        Toast.loading('資料載入中....', 3000)
    }
    loadListAndCheckNoMore = ()=>{
   // 判斷條件是取得的資料數量,小於limit。為啥要這樣,因為後端沒有返回這個欄位給我,我就只能這樣判斷咯。
        return this.props.loadFuc(this.state.queryConditionCombin).then((data:K[])=>{
            this.setState({
                noMore: data && data.length < this.state.queryConditionCombin.limit
            })
            return data;
        })
    }
    // 你懂得,哈哈哈哈哈哈哈哈哈嗝。
    scrollInBottom = ()=>{
        !this.state.noMore && this.loadMore();
    }
    // 當搜尋條件變化之後,是不是要從0開始載入呢?
    componentWillReceiveProps(nextProps:ILoadDataAndCheckMoreProps<T, K>){
        this.setState({
            queryConditionCombin: this.initQueryCondition(nextProps.queryCondition),
            noMore: false,
            list: []
        },this.loadMore)
    }
    // 第一次載入資料喔。
    componentDidMount(){
        this.loadMore()
    }
    render(){
        // 我期望渲染的方式交由業務層面
        return <>
			// 為什麼要用render作為函式傳遞呢?因為如果寫成元件你需要
            // const Cp = this.props.render
            // <Cp list={this.state.list}/>
            // 這就有點狠難受了,因為props我就想傳個陣列,但是元件不支援啊,因為他要個物件。
            // 然後你需要重新把他拎出來,根據元件的命名規範,才能重新使用
            {this.props.render(this.state.list)}
    		// 提示提示提示咯
            <p className="loadingTips">{this.state.noMore ? '這裡見底啦/(ㄒoㄒ)/~~...' : '資料載入中,請稍後...'}</p>
        </>
    }
}
// 最後是匯出,為啥要這麼寫呢?
export default <T, K>()=> scrollBottom<
    ILoadDataAndCheckMoreProps<T, K>,
    ILoadDataAndCheckMoreState<T, K>
    >(LoadDataAndCheckMore)
// 我們回看一下scrollBottom方法。
	<T extends object,S = {}>(WrapComponent: IHasScrollBottomClass<T, S>)
// 如果直接返回scrollBottom(LoadDataAndCheckMore),這個T和S會被當成簡單的{}。這樣就會造成ILoadDataAndCheckMoreProps、ILoadDataAndCheckMoreState的泛型T/K就是空物件,顯然是不正確的。

複製程式碼

就此個人對react元件封裝相關的的思路就結束啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦。。。。

相關文章