天天看點

在iOS中運用React Component的思路,效率更高的開發UI,更好的複用UI元件

最近一直在看React的一些東西,其實很早前就想開始重拾前端,但是一直提不起興趣再去看JavaScript,對CSS這種布局方式也不是很來感,說白了,就是懶吧?。去年年底開始在公司app裡開始嘗試接入Weex,是以不得不把JavaScript再重新撸了一遍,順帶着把ES6的一些新特性也了解了一下,更好的函數調用方式,Class的引入,Promise的運用等等,其實最吸引我的還是在用了Weex之後,感受到了Component帶來的UI複用,高效開發的快感。Weex是運用Vue.js來調用,渲染native控件,來達到one code, run everywhere。不管是Vue.js,還是React,最終都是朝着W3C WebComponent的标準走了(今年會釋出的Vue 3.0在元件上的文法基本上跟React一樣了)。這篇就來講講我對React Component的了解,還有怎麼把這個标準也能在native上面做運用

demo源碼

iOS UI開發的痛點

對iOS開發來說,最常用的UI元件就是UICollectionView了,就是所謂的一個清單頁,現在的app大部分頁面都是由一個清單來呈現内容的。對iOS開發者來說,我們可以封裝每個UICollectionViewCell,進而可以在每個頁面的UICollectionView中能夠複用,但是痛點是,這個複用僅僅是UI上的複用,在每寫一個新的頁面(UIViewController)的時候,還是需要建立一個UICollectionView,然後再把UICollectionView的DataSource和Delegate方法再實作一遍,把這些Cell再在這些方法裡重新生成一遍,才能讓清單展現出來。比方說我們首頁清單底部有猜你喜歡的cell,個人中心頁面底部也有猜你喜歡的cell,這兩個頁面,都需要在自己擁有的UICollectionView中注冊這個猜你喜歡的cell,傳回這個猜你喜歡cell的高度,設定這個cell的model并重新整理資料,如果有Header或者Footer的話,還得重新設定這些Header跟Footer。是以新寫一個清單頁面,對iOS開發者來說,還是很麻煩。

使用Weex或者RN開發原生清單頁

使用Weex開發清單頁的時候,我們組内的小夥伴都覺得很爽,很高效,基本上幾行代碼就能繪制出一個清單頁,舉個RN和weex的例子

// React
render() {
    const cells = this.state.items.map((item, index) => {
        if (item.cellType === 'largeCell') {
            return <LargeCell cellData={item.entity}></LargeCell>
        } else if (item.cellType === 'mediumCell') {
            return <MediumCell cellData={item.entity}></MediumCell>
        } else if (item.cellType === 'smallCell') {
            return <SmallCell cellData={item.entity}></SmallCell>
        }
    });

    return(
        <Waterfall>
            { cells }
        </Waterfall>
    );
}

// Vue
<template>
    <waterfall>
        <cell v-for="(item, index) in itemsArray" :key="index">
            <div class="cell" v-if="item.cellType === 'largeCell'">
                <LargeCell :cellData="item.entity"></LargeCell>
            </div>
            <div class="cell" v-if="item.cellType === 'mediumCell'">
                <MediumCell :cellData="item.entity"></MediumCell>
            </div>
            <div class="cell" v-if="item.cellType === 'smallCell'">
                <SmallCell :cellData="item.entity"></SmallCell>
            </div>
        </cell>
    </waterfall>
</template>

const 
           

waterfall

對應的就是iOS中的UICollectionView,waterfall這個元件中有cell的子元件,這些cell的子元件可以是我們自己定義的不同類型樣式的cell元件。

LargeCell

MediumCell

SmallCell

對應的就是原生中的我們自定義的

UICollectionViewCell

。這些Cell子組在任何

waterfall

元件下面都可以使用,在一個waterfall元件下面,我們隻需要把我們把在這個清單中需要展示的cell放進來,通過props把資料傳到cell元件中即可。這種方式對iOS開發者來說,真的是太舒服了。在覺得開發很爽的同時,我也在思考,既然這種Component的方式用起來很爽,那麼能不能也運用到原生開發中呢?畢竟我們大部分的業務需求還是基于原生來開發的。

React的核心思想

  • 先來解釋下React中的React Element和React Component
    • React Elements
      const element = <div id='login-button>Login</div>
                 
      這段JSX表達式傳回的就是一個React Element,React element描述了使用者将在螢幕上看到的那個UI,跟DOM elements不一樣的是,React elements是一個單純的對象,僅僅是對将要呈現到螢幕上的UI的一個描述,并不是真正渲染好的UI,建立一個React element開銷是極其小的,渲染的事情是由背後的React DOM來處理的。上面的那段代碼相當于:
      const element = React.createElement(
        'div',
        {id: 'login-button'},
        'Login'
      )
      
      傳回的React element對象相當于 =>
      
      {
        type: 'div',
        props: {
            children: 'Login',
            id: 'login-button'
        }
      }
                 
    • React Components

      React中最核心的一個思想就是Component了,官方的解釋是Component允許我們将UI拆分為獨立可複用的代碼片段,元件中可以包含多個其他元件,這樣将元件一個個單獨抽離出來,并最終再組合到一起,大大提高了代碼的可讀性(Readability)、可維護性(Maintainability)、可複用性(Reusability)和可測試性(Testability)。這也是 React 裡用 Component 抽象所有 UI 的意義所在。

      class Button extends React.Component {
        render() {
          const element = <div id='login-button>{ this.props.title }</div>
          return (
            <div>
              { element }
            </div>
          )
      }
                 
      這段代碼中Button就是一個React Component,這個component接受一個叫props的參數,傳回描述UI的React element。
  • 可以看出React Component接受props是一個對象,也就是所謂的一種資料結構,傳回React Element也是一種對象,所謂的另外一種資料結構,是以我認為的React Component其實就是一個function,這個function的主要功能就是将一種資料結構(描述原始資料)轉換成另外一種資料結構(描述UI)。React element僅僅是一個描述UI的對象,可以認為是一個中間狀态,我們可以用最小的開銷來建立或者銷毀element對象。
  • React的核心思想總結下來就是這樣的一個流程
    1. 原始資料到UI資料的轉化 props -> React Component -> React Element
    2. React Element的作用是将Component的建立跟描述狀态分離,Component内部主要負責這個Component的建構,React Element主要用來做描述這個Component的狀态
    3. 多個Component傳回的多個Elements,這個流程是進行UI組合
    4. React Element并不是一個渲染結果,React DOM的作用是将UI的狀态(即Element)和UI的渲染分離,React DOM負責element的渲染
    5. 最後一個流程就是UI渲染了
  • 上述這幾個流程基本上代表了React的核心概念

怎麼在iOS中運用React Component概念

  • 說了這麼多,其實iOS中缺少的就是這個Component概念,iOS原生的流程是原始資料到UI布局,再到UI繪制。複用的隻是UI繪制結果的那個view(e.g. UICollectionViewCell)
  • 在使用UICollectionView的時候,我們的資料都是通過DataSource方法傳回給UICollectionView,UICollectionView拿到這些資料之後,就直接去繪制UICollectionViewCell了。是以每個清單頁都得重建立一個UICollectionView,再引入自定義的UICollectionViewCell來繪制清單,所有的DataSource跟Delegate方法都得走一遍。是以我在想,我們可以按照React的那種方式來繪制清單麼?将一個個UI控件抽象成一個個元件,再将這些元件組合到一起,繪制出最後的頁面,React或者Weex的繪制清單其實就是waterfall這個清單component裡面按照清單順序插入自定義的cell component(組合)。那麼我們其實可以在iOS中也可以有這個waterfall的component,這個component支援一個

    insertChildComponent:

    的方法,這個方法裡就是插入自定義的CellComponent到waterfall這個元件中,并通過傳入props來建立這個component。是以我就先定義了一個元件的基類BaseComponent
    @protocol ComponentProtocol <NSObject>
    
    /**
     *  繪制元件
     *
     *  @param view 展示該元件的view
     */
    - (void)drawComponentInView:(UIView *)view withProps:(id)props;
    
    /**
     *  元件的尺寸
     *
     *  @param props 該component的資料model
     *  @return 該元件的size
     */
    + (CGSize)componentSize:(id)props;
    
    @end
    
    @interface BaseComponent : NSObject <ComponentProtocol>
    
    - (instancetype)initWithProps:(id)props;
    
    @property (nonatomic, strong, readonly) id props;
    
               
    所有的Component的建立都是通過傳入props參數,來傳回一個元件執行個體,每個Component還遵守一個

    ComponentProtocol

    的協定,協定裡兩個方法:
    • - (void)drawComponentInView:(UIView *)view withProps:(id)props;

      每個component通過這個方法來進行native控件的繪制,參數中

      view

      是将會展示該元件的view,比方說WaterfallComponent中的該方法view為UIViewController的view,因為UIViewController的view會用來展示WaterfallComponent這個元件,'props'是該元件建立時傳入的參數,這個參數用來告訴元件應該怎樣繪制UI
    • + (CGSize)componentSize:(id)props;

      來描述元件的尺寸。
  • 有了這個Component概念之後,我們原生的繪制流程就變成
    1. 建立Component,傳入參數props
    2. Component内部執行建立代碼,儲存props
    3. 當頁面需要繪制的時候(React中的render指令),component内部會執行

      - (void)drawComponentInView:(UIView *)view withProps:(id)props;

      方法來描述并繪制UI
  • 原生代碼中想實作React element,其實不是一件簡單的事情,因為原生沒有類似JSX這種語言來生成一套隻用來描述UI,并不繪制UI的中間狀态的對象(可以做,比方說自己定義一套文法來描述UI),是以目前我的做法是在component内部,等到繪制指令來了之後,通過在

    - (void)drawComponentInView:(UIView *)view withProps:(id)props

    方法中,調用原生自定義的UIKit控件,通過props來繪制該UIKit
  • 是以将通過封裝component的方式,我們之前UIKit代表的UI元件轉換成元件,把這些元件一個個單獨抽離出來,再通過搭積木的方式,将各種元件一個個組合到一起,怎麼繪制交給component内部去描述,而不是交給每個頁面對應的UIViewController

Demo

Demo中,我會建立一個WaterfallComponent元件,還有多個CellComponent來繪制清單頁,每個不一樣清單頁面(UIViewController)都可以建立一個WaterfallComponent元件,然後将不一樣的CellComponent按照順序插入到WaterfallComponent元件中,即可完成繪制清單,不需要每個頁面再去處理UICollectionView的DataSource,Delegate方法。

在iOS中運用React Component的思路,效率更高的開發UI,更好的複用UI元件

Untitled.png

WaterfallComponent内部會有一個UICollectionView,WaterfallComponent的insertChildComponent方法中,會建立一個dataController來管理資料源,并用來跟UICollectionView的DataSource方法進行互動進而繪制出清單頁,最終UIViewController中繪制清單的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];
    
for (NSDictionary *props in datas) {
    if ([props[@"type"] isEqualToString:@"1"]) {
        FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props];
        [self.waterfallComponent insertChildComponent:cellComponent];
    } else if ([props[@"type"] isEqualToString:@"2"]) {
        SecondCellComponent *cellComponent = [[SecondCellComponent alloc] initWithProps:props];
        [self.waterfallComponent insertChildComponent:cellComponent];
    }
}
[self.waterfallComponent drawComponentInView:self.view withProps:nil];
           

這樣,每個我們自定義的Cell就可以以CellComponent的形式,被按照随意順序插入到WaterfallComponent,進而做到了真正意義上的複用,Demo已上傳到GitHub上,有興趣的可以看看

總結

  • React的核心思想是将元件一個個單獨抽離出來,并最終再組合到一起,大大提高了代碼的可讀性、可維護性、可複用性和可測試性。這也是 React 裡用 Component 抽象所有 UI 的意義所在。
  • 原生開發中,使用Component的概念,用Component去抽象UIKit控件,也能達到同樣的效果,這樣也能統一每個開發使用UICollectionView時候的規範,也能統一對所有清單頁的資料源做一些統一處理,比方說根據一個邏輯,統一在所有清單頁,插入一個廣告cell,這個邏輯完全可以在WaterfallComponent裡統一處理。

思考

目前我們隻用到了Component這個概念,其實React中,React Element的概念也是非常核心的,React Element隔離了UI描述跟UI繪制的邏輯,通過JSX來描述UI,并不去生成,繪制UI,這樣我們能夠以最小的代價來生成或者銷毀React Elements,然後在傳遞給系統繪制elements裡描述的UI,那麼如果原生裡也有這一套模闆語言,那麼我們就能真正做到在Component裡,傳入props,傳回一個element描述UI,然後再交給系統去繪制,這樣還能省去cell的建立,隻建立CellComponent即可。其實我們可以通過定義一套語義去描述UI布局,然後通過解析這套語義,通過Core Text去做繪制,這一套還是值得我再去思考的。

作者:Kobe_Dai

連結:https://www.jianshu.com/p/bc4b13a0d312

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。