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

Geek.發表於2019-04-28

最近一直在看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的例子

// Reactrender() { const cells = this.state.items.map((item, index) => { if (item.cellType === 'largeCell') { return} else if (item.cellType === 'mediumCell') { return} else if (item.cellType === 'smallCell') { return} }); return({ cells });}// Vueconst

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

constelement =

這段JSX表示式返回的就是一個React Element,React element描述了使用者將在螢幕上看到的那個UI,跟DOM elements不一樣的是,React elements是一個單純的物件,僅僅是對將要呈現到螢幕上的UI的一個描述,並不是真正渲染好的UI,建立一個React element開銷是極其小的,渲染的事情是由背後的React DOM來處理的。上面的那段程式碼相當於:

constelement = 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 的意義所在。

classButtonextendsReact.Component{ render() {constelement =

    { 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的核心思想總結下來就是這樣的一個流程

原始資料到UI資料的轉化 props -> React Component -> React Element

React Element的作用是將Component的建立跟描述狀態分離,Component內部主要負責這個Component的構建,React Element主要用來做描述這個Component的狀態

多個Component返回的多個Elements,這個流程是進行UI組合

React Element並不是一個渲染結果,React DOM的作用是將UI的狀態(即Element)和UI的渲染分離,React DOM負責element的渲染

最後一個流程就是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

@protocolComponentProtocol/**

  • 繪製元件

  • @param view 展示該元件的view

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

  • 元件的尺寸

  • @param props 該component的資料model

  • @return 該元件的size

*/+ (CGSize)componentSize:(id)props;@end@interfaceBaseComponent:NSObject- (instancetype)initWithProps:(id)props;@property(nonatomic,strong,readonly)idprops;

所有的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概念之後,我們原生的繪製流程就變成

建立Component,傳入引數props

Component內部執行建立程式碼,儲存props

當頁面需要繪製的時候(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方法。

Untitled.png

WaterfallComponent內部會有一個UICollectionView,WaterfallComponent的insertChildComponent方法中,會建立一個dataController來管理資料來源,並用來跟UICollectionView的DataSource方法進行互動從而繪製出列表頁,最終UIViewController中繪製列表的方法如下:

self.waterfallComponent = [[WaterfallComponent alloc] initWithProps:nil];for(NSDictionary*propsindatas) {if([props[@"type"] isEqualToString:@"1"]) { FirstCellComponent *cellComponent = [[FirstCellComponent alloc] initWithProps:props]; [self.waterfallComponent insertChildComponent:cellComponent]; }elseif([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去做繪製,這一套還是值得我再去思考的。

本文源於第三方轉載,原文連結:www.jianshu.com/p/bc4b13a0d…

文章若有不對地方,歡迎批評指正,一個小而有用QQ交流群:805558511

相關文章