最近一直在看React的一些東西,其實很早前就想開始重拾前端,但是一直提不起興趣再去看JavaScript,對CSS這種布局方式也不是很來感,說白了,就是懶吧??。去年年底開始在公司app里開始嘗試接入Weex,所以不得不把JavaScript再重新擼了一遍,順帶著把ES6的一些新特性也了解了一下,更好的函數(shù)調(diào)用方式,Class的引入,Promise的運用等等,其實最吸引我的還是在用了Weex之后,感受到了Component帶來的UI復(fù)用,高效開發(fā)的快感。Weex是運用Vue.js來調(diào)用,渲染native控件,來達(dá)到one code, run everywhere。不管是Vue.js,還是React,最終都是朝著W3C WebComponent的標(biāo)準(zhǔn)走了(今年會發(fā)布的Vue 3.0在組件上的語法基本上跟React一樣了)。這篇就來講講我對React Component的理解,還有怎么把這個標(biāo)準(zhǔn)也能在native上面做運用
iOS UI開發(fā)的痛點
對iOS開發(fā)來說,最常用的UI組件就是UICollectionView了,就是所謂的一個列表頁,現(xiàn)在的app大部分頁面都是由一個列表來呈現(xiàn)內(nèi)容的。對iOS開發(fā)者來說,我們可以封裝每個UICollectionViewCell,從而可以在每個頁面的UICollectionView中能夠復(fù)用,但是痛點是,這個復(fù)用僅僅是UI上的復(fù)用,在每寫一個新的頁面(UIViewController)的時候,還是需要新建一個UICollectionView,然后再把UICollectionView的DataSource和Delegate方法再實現(xiàn)一遍,把這些Cell再在這些方法里重新生成一遍,才能讓列表展現(xiàn)出來。比方說我們首頁列表底部有猜你喜歡的cell,個人中心頁面底部也有猜你喜歡的cell,這兩個頁面,都需要在自己擁有的UICollectionView中注冊這個猜你喜歡的cell,返回這個猜你喜歡cell的高度,設(shè)置這個cell的model并刷新數(shù)據(jù),如果有Header或者Footer的話,還得重新設(shè)置這些Header跟Footer。所以新寫一個列表頁面,對iOS開發(fā)者來說,還是很麻煩。
使用Weex或者RN開發(fā)原生列表頁
使用Weex開發(fā)列表頁的時候,我們組內(nèi)的小伙伴都覺得很爽,很高效,基本上幾行代碼就能繪制出一個列表頁,舉個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
對應(yīng)的就是iOS中的UICollectionView,waterfall這個組件中有cell的子組件,這些cell的子組件可以是我們自己定義的不同類型樣式的cell組件。LargeCell
,MediumCell
,SmallCell
對應(yīng)的就是原生中的我們自定義的UICollectionViewCell
。這些Cell子組在任何waterfall
組件下面都可以使用,在一個waterfall組件下面,我們只需要把我們把在這個列表中需要展示的cell放進來,通過props把數(shù)據(jù)傳到cell組件中即可。這種方式對iOS開發(fā)者來說,真的是太舒服了。在覺得開發(fā)很爽的同時,我也在思考,既然這種Component的方式用起來很爽,那么能不能也運用到原生開發(fā)中呢?畢竟我們大部分的業(yè)務(wù)需求還是基于原生來開發(fā)的。
React的核心思想
- 先來解釋下React中的React Element和React Component
- React Elements
這段JSX表達(dá)式返回的就是一個React Element,React element描述了用戶將在屏幕上看到的那個UI,跟DOM elements不一樣的是,React elements是一個單純的對象,僅僅是對將要呈現(xiàn)到屏幕上的UI的一個描述,并不是真正渲染好的UI,創(chuàng)建一個React element開銷是極其小的,渲染的事情是由背后的React DOM來處理的。上面的那段代碼相當(dāng)于:const element = <div id='login-button>Login</div>
const element = React.createElement( 'div', {id: 'login-button'}, 'Login' ) 返回的React element對象相當(dāng)于 => { type: 'div', props: { children: 'Login', id: 'login-button' } }
- React Components
React中最核心的一個思想就是Component了,官方的解釋是Component允許我們將UI拆分為獨立可復(fù)用的代碼片段,組件中可以包含多個其他組件,這樣將組件一個個單獨抽離出來,并最終再組合到一起,大大提高了代碼的可讀性(Readability)、可維護性(Maintainability)、可復(fù)用性(Reusability)和可測試性(Testability)。這也是 React 里用 Component 抽象所有 UI 的意義所在。
這段代碼中Button就是一個React Component,這個component接受一個叫props的參數(shù),返回描述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是一個對象,也就是所謂的一種數(shù)據(jù)結(jié)構(gòu),返回React Element也是一種對象,所謂的另外一種數(shù)據(jù)結(jié)構(gòu),所以我認(rèn)為的React Component其實就是一個function,這個function的主要功能就是將一種數(shù)據(jù)結(jié)構(gòu)(描述原始數(shù)據(jù))轉(zhuǎn)換成另外一種數(shù)據(jù)結(jié)構(gòu)(描述UI)。React element僅僅是一個描述UI的對象,可以認(rèn)為是一個中間狀態(tài),我們可以用最小的開銷來創(chuàng)建或者銷毀element對象。
- React的核心思想總結(jié)下來就是這樣的一個流程
- 原始數(shù)據(jù)到UI數(shù)據(jù)的轉(zhuǎn)化 props -> React Component -> React Element
- React Element的作用是將Component的創(chuàng)建跟描述狀態(tài)分離,Component內(nèi)部主要負(fù)責(zé)這個Component的構(gòu)建,React Element主要用來做描述這個Component的狀態(tài)
- 多個Component返回的多個Elements,這個流程是進行UI組合
- React Element并不是一個渲染結(jié)果,React DOM的作用是將UI的狀態(tài)(即Element)和UI的渲染分離,React DOM負(fù)責(zé)element的渲染
- 最后一個流程就是UI渲染了
- 上述這幾個流程基本上代表了React的核心概念
怎么在iOS中運用React Component概念
說了這么多,其實iOS中缺少的就是這個Component概念,iOS原生的流程是原始數(shù)據(jù)到UI布局,再到UI繪制。復(fù)用的只是UI繪制結(jié)果的那個view(e.g. UICollectionViewCell)
-
在使用UICollectionView的時候,我們的數(shù)據(jù)都是通過DataSource方法返回給UICollectionView,UICollectionView拿到這些數(shù)據(jù)之后,就直接去繪制UICollectionViewCell了。所以每個列表頁都得重新建一個UICollectionView,再引入自定義的UICollectionViewCell來繪制列表,所有的DataSource跟Delegate方法都得走一遍。所以我在想,我們可以按照React的那種方式來繪制列表么?將一個個UI控件抽象成一個個組件,再將這些組件組合到一起,繪制出最后的頁面,React或者Weex的繪制列表其實就是waterfall這個列表component里面按照列表順序插入自定義的cell component(組合)。那么我們其實可以在iOS中也可以有這個waterfall的component,這個component支持一個
insertChildComponent:
的方法,這個方法里就是插入自定義的CellComponent到waterfall這個組件中,并通過傳入props來創(chuàng)建這個component。所以我就先定義了一個組件的基類BaseComponent@protocol ComponentProtocol <NSObject> /** * 繪制組件 * * @param view 展示該組件的view */ - (void)drawComponentInView:(UIView *)view withProps:(id)props; /** * 組件的尺寸 * * @param props 該component的數(shù)據(jù)model * @return 該組件的size */ + (CGSize)componentSize:(id)props; @end @interface BaseComponent : NSObject <ComponentProtocol> - (instancetype)initWithProps:(id)props; @property (nonatomic, strong, readonly) id props;
所有的Component的創(chuàng)建都是通過傳入props參數(shù),來返回一個組件實例,每個Component還遵守一個
ComponentProtocol
的協(xié)議,協(xié)議里兩個方法:-
- (void)drawComponentInView:(UIView *)view withProps:(id)props;
每個component通過這個方法來進行native控件的繪制,參數(shù)中view
是將會展示該組件的view,比方說WaterfallComponent中的該方法view為UIViewController的view,因為UIViewController的view會用來展示W(wǎng)aterfallComponent這個組件,'props'是該組件創(chuàng)建時傳入的參數(shù),這個參數(shù)用來告訴組件應(yīng)該怎樣繪制UI -
+ (CGSize)componentSize:(id)props;
來描述組件的尺寸。
-
-
有了這個Component概念之后,我們原生的繪制流程就變成
- 創(chuàng)建Component,傳入?yún)?shù)props
- Component內(nèi)部執(zhí)行創(chuàng)建代碼,保存props
- 當(dāng)頁面需要繪制的時候(React中的render命令),component內(nèi)部會執(zhí)行
- (void)drawComponentInView:(UIView *)view withProps:(id)props;
方法來描述并繪制UI
原生代碼中想實現(xiàn)React element,其實不是一件簡單的事情,因為原生沒有類似JSX這種語言來生成一套只用來描述UI,并不繪制UI的中間狀態(tài)的對象(可以做,比方說自己定義一套語法來描述UI),所以目前我的做法是在component內(nèi)部,等到繪制命令來了之后,通過在
- (void)drawComponentInView:(UIView *)view withProps:(id)props
方法中,調(diào)用原生自定義的UIKit控件,通過props來繪制該UIKit所以將通過封裝component的方式,我們之前UIKit代表的UI組件轉(zhuǎn)換成組件,把這些組件一個個單獨抽離出來,再通過搭積木的方式,將各種組件一個個組合到一起,怎么繪制交給component內(nèi)部去描述,而不是交給每個頁面對應(yīng)的UIViewController
Demo
Demo中,我會創(chuàng)建一個WaterfallComponent組件,還有多個CellComponent來繪制列表頁,每個不一樣列表頁面(UIViewController)都可以創(chuàng)建一個WaterfallComponent組件,然后將不一樣的CellComponent按照順序插入到WaterfallComponent組件中,即可完成繪制列表,不需要每個頁面再去處理UICollectionView的DataSource,Delegate方法。
WaterfallComponent內(nèi)部會有一個UICollectionView,WaterfallComponent的insertChildComponent方法中,會創(chuàng)建一個dataController來管理數(shù)據(jù)源,并用來跟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,從而做到了真正意義上的復(fù)用,Demo已上傳到GitHub上,有興趣的可以看看
總結(jié)
- React的核心思想是將組件一個個單獨抽離出來,并最終再組合到一起,大大提高了代碼的可讀性、可維護性、可復(fù)用性和可測試性。這也是 React 里用 Component 抽象所有 UI 的意義所在。
- 原生開發(fā)中,使用Component的概念,用Component去抽象UIKit控件,也能達(dá)到同樣的效果,這樣也能統(tǒng)一每個開發(fā)使用UICollectionView時候的規(guī)范,也能統(tǒng)一對所有列表頁的數(shù)據(jù)源做一些統(tǒng)一處理,比方說根據(jù)一個邏輯,統(tǒng)一在所有列表頁,插入一個廣告cell,這個邏輯完全可以在WaterfallComponent里統(tǒng)一處理。
思考
目前我們只用到了Component這個概念,其實React中,React Element的概念也是非常核心的,React Element隔離了UI描述跟UI繪制的邏輯,通過JSX來描述UI,并不去生成,繪制UI,這樣我們能夠以最小的代價來生成或者銷毀React Elements,然后在交付給系統(tǒng)繪制elements里描述的UI,那么如果原生里也有這一套模板語言,那么我們就能真正做到在Component里,傳入props,返回一個element描述UI,然后再交給系統(tǒng)去繪制,這樣還能省去cell的創(chuàng)建,只創(chuàng)建CellComponent即可。其實我們可以通過定義一套語義去描述UI布局,然后通過解析這套語義,通過Core Text去做繪制,這一套還是值得我再去思考的。