最近一直在看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
這段JSX表達式傳回的就是一個React Element,React element描述了使用者将在螢幕上看到的那個UI,跟DOM elements不一樣的是,React elements是一個單純的對象,僅僅是對将要呈現到螢幕上的UI的一個描述,并不是真正渲染好的UI,建立一個React element開銷是極其小的,渲染的事情是由背後的React DOM來處理的。上面的那段代碼相當于:const element = <div id='login-button>Login</div>
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 的意義所在。
這段代碼中Button就是一個React Component,這個component接受一個叫props的參數,傳回描述UI的React element。class Button extends React.Component { render() { const element = <div id='login-button>{ this.props.title }</div> return ( <div> { element } </div> ) }
- React Elements
- 可以看出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支援一個
的方法,這個方法裡就是插入自定義的CellComponent到waterfall這個元件中,并通過傳入props來建立這個component。是以我就先定義了一個元件的基類BaseComponentinsertChildComponent:
所有的Component的建立都是通過傳入props參數,來傳回一個元件執行個體,每個Component還遵守一個@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;
的協定,協定裡兩個方法:ComponentProtocol
-
每個component通過這個方法來進行native控件的繪制,參數中- (void)drawComponentInView:(UIView *)view withProps:(id)props;
是将會展示該元件的view,比方說WaterfallComponent中的該方法view為UIViewController的view,因為UIViewController的view會用來展示WaterfallComponent這個元件,'props'是該元件建立時傳入的參數,這個參數用來告訴元件應該怎樣繪制UIview
-
來描述元件的尺寸。+ (CGSize)componentSize:(id)props;
-
- 有了這個Component概念之後,我們原生的繪制流程就變成
- 建立Component,傳入參數props
- Component内部執行建立代碼,儲存props
- 當頁面需要繪制的時候(React中的render指令),component内部會執行
方法來描述并繪制UI- (void)drawComponentInView:(UIView *)view withProps:(id)props;
- 原生代碼中想實作React element,其實不是一件簡單的事情,因為原生沒有類似JSX這種語言來生成一套隻用來描述UI,并不繪制UI的中間狀态的對象(可以做,比方說自己定義一套文法來描述UI),是以目前我的做法是在component内部,等到繪制指令來了之後,通過在
方法中,調用原生自定義的UIKit控件,通過props來繪制該UIKit- (void)drawComponentInView:(UIView *)view withProps:(id)props
- 是以将通過封裝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 *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
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。