一慈格、框架
1.1、技術(shù)選型及思考
首先需要明確定位以及邊界蛋褥,我們需要一個(gè)怎么樣的框架去解決存在的痛點(diǎn)临燃。
需要的:
? 由于在首頁(yè)場(chǎng)景使用,高性能和穩(wěn)定性是最基本的要求烙心;
? 為了不跟隨版本發(fā)布膜廊,所以動(dòng)態(tài)性也是要考慮的;
? 為了解決研發(fā)成本淫茵,多端渲染也是需要解決的問(wèn)題爪瓜;
不考慮的場(chǎng)景:
? 不需要處理復(fù)雜的業(yè)務(wù)邏輯;
? 不支持動(dòng)畫(huà)精細(xì)的交互場(chǎng)景匙瘪;
? 不考慮多個(gè)組件的聯(lián)動(dòng)性铆铆;
通過(guò)梳理場(chǎng)景和邊界使得目標(biāo)清晰蝶缀。我們需要一個(gè)跨平臺(tái)支持動(dòng)態(tài)性并且高性能 UI 渲染框架。
站在自身需求的角度薄货,調(diào)研業(yè)界成熟的方案得出的結(jié)論如下表翁都。
React Native:動(dòng)態(tài)性高,但是學(xué)習(xí)成本和性能(加載性能菲驴、頁(yè)面性能)不理想荐吵;
Flutter:谷歌的跨平臺(tái)框架,性能高赊瞬,但是無(wú)動(dòng)態(tài)性先煎;
通過(guò)以上的調(diào)研,我們打算用 Native 解析 JSON + Flexbox 的方式來(lái)作為最終方案巧涧。
這么做有什么優(yōu)勢(shì)薯蝎?
1)學(xué)習(xí)成本低:Flexbox 布局方式被開(kāi)發(fā)廣泛接受(內(nèi)部跨平臺(tái)技術(shù)棧用的多的是 RN);
2)開(kāi)發(fā)成本低:JSON 和 Flexbox(Yoga)都有成熟的高性能可靠的第三庫(kù)直接使用谤绳,加快框架開(kāi)發(fā)速度(在一個(gè)月內(nèi)將框架完成并且上線)占锯;
3)兼容性強(qiáng):Flexbox 完美兼容 Web 端布局的方式,F(xiàn)oxPage 同時(shí)支持 Web 端的 DSL 的輸出缩筛;
4)自定義&擴(kuò)展強(qiáng):由于自研消略,沒(méi)有包袱,可以在設(shè)計(jì)上以最符合我們的場(chǎng)景來(lái)設(shè)計(jì)框架瞎抛;
1.2艺演、架構(gòu)設(shè)計(jì)
如何做好架構(gòu)設(shè)計(jì),可以先了解下 Chrome 是如何完成一個(gè) HTML 到 UI 的輸出桐臊。
那么 Flutter 渲染流程是如何呢胎撤?
通過(guò)調(diào)研沉淀下我們的渲染流程:
各個(gè)模塊的職責(zé)清晰且獨(dú)立:
Downloader:主要負(fù)責(zé) DSL 更新與下載。
CacheManager:顧名思義断凶,負(fù)責(zé) DSL 的緩存管理伤提。
Parse:這層主要是做 DSL 解析,負(fù)責(zé)將 JSON 數(shù)據(jù)組織成節(jié)點(diǎn)认烁,供下層使用肿男。
Layout:此層職責(zé)為將 Parse 模塊解析之后的數(shù)據(jù)計(jì)算布局,生成布局元素却嗡。
Draw:此層職責(zé)為將 Parse 生成節(jié)點(diǎn)設(shè)置 Layout 層的布局信息輸出 Native 視圖樹(shù)并提交系統(tǒng)完成渲染次伶。
遵守職責(zé)化最小原則,每個(gè)模塊被賦予的職責(zé)最小化并且彼此獨(dú)立稽穆,使后續(xù)的維護(hù)冠王,可以將影響范圍限制模塊內(nèi)部,而不影響其他模塊的穩(wěn)定性舌镶,增強(qiáng)系統(tǒng)維護(hù)性柱彻。也使得對(duì)模塊定制優(yōu)化提供基礎(chǔ)豪娜。
下文會(huì)繼續(xù)針對(duì)框架內(nèi)部原理做深入介紹。
1.3哟楷、DSL 的定義
數(shù)據(jù)綁定
想象一下瘤载,在我們?nèi)粘i_(kāi)發(fā)中,往往是數(shù)據(jù)對(duì)應(yīng)一個(gè) UI 元素的顯示卖擅,需要有一定的綁定數(shù)據(jù)機(jī)制鸣奔。
{
"parentId":"cfb87570-82d8-11e9-811f-0906d1cca8d4",
"name": "image",
"props": {
"url":"{{product.imageUrl}}",
},
"extendId": "",
"conditions": [],
"type": "trip-app.image",
}
變量的格式為{{變量名}},如以上示例:我們的 image 組件中 url 的屬性被設(shè)置為 product 對(duì)象屬性中的 imageUrl 的值惩阶。
{
"parentId":"cfb87570-82d8-11e9-811f-0906d1cca8d4",
"name": "image",
"props": {
"url":"{{products[0].imageUrl}}",
},
"extendId": "",
"conditions": [],
"type":"trip-app.image",
}
數(shù)組取值格式為{{數(shù)組[Index]}}挎狸,如上,我們可以通過(guò)此方法獲取 products 數(shù)組中的第一個(gè)元素的圖片断楷。
事件
在組件觸發(fā)事件的時(shí)侯锨匆,我們希望能做一些自定義的事情,如跳轉(zhuǎn)頁(yè)面冬筒,怎么定義呢恐锣?
{
"onClick": {
"name":"router_call",
"props": {
"value": {
"plugin":"router",
"method":"openURL",
"args": {
"url":"{{products.deeplink}}"
}
}
},
"type":"function.call",
}
}
以上示例表示在點(diǎn)擊事件中通過(guò) router 中的 openURL 打開(kāi)了一個(gè)新的頁(yè)面。
條件判斷
在某些條件成立才渲染的場(chǎng)景下舞痰,我們也提供了條件判斷土榴,示例:
{
"props": {
"hidden": {
"name":"render-activity",
"type": 1,
"props": {
"items": [
{
"key":"{{data.showActivities}}",
"operation":"eq",
"value":"1"
}
]
}
}
},
"type":"trip-app.image",
}
如上示例: image 組件是否隱藏通過(guò){{data.showActivities}} =="1"來(lái)控制。
埋點(diǎn)機(jī)制
我們還定義了動(dòng)態(tài)埋點(diǎn)的一些規(guī)范响牛,如示例:
{
"name": "image",
"props": {
"$traceData": {
"onClick": {
"eventName":"home.click.deals.item",
"data": {
"url":"{{data.operatingActivities.0.deeplink}}",
"type":"operating_activities"
}
}
}
},
"type":"trip-app.image"
}
在 image 組件中聲明了點(diǎn)擊事件玷禽,并且把需要的參數(shù),通過(guò) data 字段一并上傳服務(wù)端娃善。
1.4论衍、布局
我們的目標(biāo)是為了解決動(dòng)態(tài)性和多端一致性瑞佩,那么具備一個(gè)完備的布局能力是一個(gè)基礎(chǔ)要求聚磺。通過(guò)調(diào)研,有以下3種方案可選炬丸。
1)自定義:完全自定義一套規(guī)則瘫寝,實(shí)現(xiàn)成本高,布局效率取決于實(shí)現(xiàn)程度稠炬,所以這邊是“中”焕阿,因?yàn)槭亲远x,所有通用性是三者最差的首启,幾乎獨(dú)家專屬暮屡。
2)Web CSS:實(shí)現(xiàn)一套 Web CSS 樣式集,可想而知毅桃,如果實(shí)現(xiàn)這樣的一套系統(tǒng)代價(jià)是極高的褒纲,為了兼容眾多的 CSS 樣式准夷,布局效率必然會(huì)下降,但是此方案通用性也是最佳莺掠,多端共享衫嵌。
3)Flexbox:彈性盒子布局,從 Web CSS 子集發(fā)展而來(lái)彻秆,在 RN 已得到充分證明它的適用性楔绞,由于 Yoga 的存在,讓我們?cè)趯?shí)現(xiàn)成本上得到下降唇兑。
Yoga 是 Facebook 基于 Flexbox 的跨平臺(tái)布局引擎開(kāi)源庫(kù)酒朵,被用于 RN,Weex 等項(xiàng)目中幔亥,也證明了其高性能和可靠性耻讽。
在實(shí)際使用過(guò)程中,Yoga 在 Android 中發(fā)現(xiàn)了一些問(wèn)題帕棉,不過(guò)我們通過(guò)定制源碼完美解決针肥,并且在實(shí)際體驗(yàn)下來(lái) Yoga 完美的勝任了布局的任務(wù)。
一些布局示例:
"props": {
"$layoutStyle": {
"position":"absolute",
"bottom": "12",
"flexDirection":"column",
"marginLeft": "8",
"width": "100%",
"paddingRight":"16"
}
}
單位定義
width香伴、height 等實(shí)際數(shù)字單位定義中慰枕,我們定義了如下的數(shù)字單位。
1.5即纲、DSL 解析
解析層具帮,要做的事情比較簡(jiǎn)單,為了提高性能低斋,并且對(duì)于相同的 DSL 模板蜂厅,只會(huì)做一次解析,之后便會(huì)把結(jié)果做一個(gè)緩存膊畴,以下的流程圖代表著解析流程掘猿。
1.6、視圖構(gòu)建
視圖構(gòu)建相對(duì)簡(jiǎn)單唇跨,通過(guò)解析層解析之后稠通,每個(gè)視圖組件都會(huì)ViewNode節(jié)點(diǎn)一一對(duì)應(yīng)視圖在虛擬視圖樹(shù)中的狀態(tài),包括了視圖布局屬性买猖,視圖屬性等元素信息改橘。
以下為 iOS 代碼示例:
/**
視圖節(jié)點(diǎn),映射 FoxPage 中的組件
*/
@interfaceFPViewNode : NSObject
/**
視圖屬性
*/
@property(nonatomic, strong) FPViewAttribute *attribute;
/**
子視圖
*/
@property(nonatomic, copy) NSArray<FPViewNode *> *children;
/**
綁定關(guān)系的值
*/
@property(nonatomic, copy, readonly) NSArray <NSDictionary *> *bindValues;
/**
綁定關(guān)系的值
*/
@property(nonatomic, strong) NSMutableArray<NSString *> *conditions;
/**
真正的視圖引用
*/
@property(nonatomic, weak) UIView<FPViewComponentProtocol> *view;
/**
添加的父視圖
*/
@property(nonatomic, weak) UIView<FPViewComponentProtocol> *superView;
@end
在ViewNode樹(shù)準(zhǔn)備好之后玉控,我們會(huì)將樹(shù)傳遞到渲染層中進(jìn)行渲染操作飞主,在渲染層中,根據(jù)ViewNode節(jié)點(diǎn)的類型,通過(guò)代理的方式碌识,從注冊(cè)的組件之中創(chuàng)建出視圖實(shí)例讽挟,配合 Yoga 布局屬性,轉(zhuǎn)換到 Native 視圖的映射丸冕,由系統(tǒng)完成最終的渲染耽梅。
1.7、數(shù)據(jù)更新
在解析渲染完成之后胖烛,關(guān)于數(shù)據(jù)流是怎么處理的呢眼姐?以下是處理流程圖:
為了優(yōu)化性能,我們針對(duì) UI 元素有變化的部分做 dirty 處理佩番,會(huì)觸發(fā) Layout 和 Draw 模塊重計(jì)算和重繪众旗。
1.8、動(dòng)態(tài)更新
動(dòng)態(tài)更新能力是重要的一環(huán)趟畏,在云端更新了頁(yè)面布局或者樣式之后贡歧,App 需要即時(shí)拉取到最新的 DSL 模板。以下是流程中的時(shí)序圖:
需要注意幾點(diǎn):
1)App 打包需要把線上目前可用的 DSL 模板打包進(jìn) App 中赋秀,避免第一次打開(kāi) App DSL 模板未下載的時(shí)候的空窗口現(xiàn)象利朵;
2)版本升級(jí)需要做好數(shù)據(jù)隔離和清除;
3)DSL 最新版本下發(fā)猎莲,需要做好 backup 與異常校驗(yàn)绍弟;
通過(guò)動(dòng)態(tài)更新機(jī)制,改變了我們發(fā)布需要跟隨版本的痛點(diǎn)著洼,有問(wèn)題樟遣,修復(fù)之后可以直接下發(fā)到用戶的 App。
1.9身笤、可視化頁(yè)面搭建平臺(tái)
看到這里的看官豹悬,心中肯定會(huì)有疑問(wèn)?你們提供變量液荸、布局瞻佛、事件、埋點(diǎn)追蹤莹弊,條件搓逾,手動(dòng)來(lái)寫(xiě)這很模板復(fù)雜度肯定是很高的哀九,不是普通人可以勝任的吧。
是的拜马,我們也意識(shí)到了此問(wèn)題考抄,所以配套了一套可視化的編輯界面细疚。如下面示例圖:
左邊是可視化編輯頁(yè)面,右側(cè)為實(shí)際在 App 場(chǎng)景的使用效果川梅,可以看出還原度還是很高的疯兼。
屬性編輯界面:
二然遏、頁(yè)面工程化的轉(zhuǎn)變
通過(guò)動(dòng)態(tài)化的轉(zhuǎn)變之后,首頁(yè)的業(yè)務(wù)需求開(kāi)發(fā)的工程模式與研發(fā)流程也由此發(fā)生變化吧彪。
在舊模式下待侵,研發(fā)人員更加關(guān)注業(yè)務(wù)需求如何實(shí)現(xiàn),首頁(yè)的業(yè)務(wù)需求如何在已有的框架體系之內(nèi)跑起來(lái)姨裸。新模式下秧倾,研發(fā)人員更注重的是,業(yè)務(wù)組件如何設(shè)計(jì)傀缩,如何完成的一個(gè)高質(zhì)量的業(yè)務(wù)組件那先。
將研發(fā)人員關(guān)注的復(fù)雜度從面降為點(diǎn),使得首頁(yè)的各個(gè)業(yè)務(wù)模塊之間的獨(dú)立性更高赡艰,以及更加穩(wěn)定售淡。模塊之間的復(fù)用性提升,如其他業(yè)務(wù)部門(mén)也想用廣告組件慷垮,他們只需要在其頁(yè)面做些簡(jiǎn)單的配置揖闸。
在組件生態(tài)不斷補(bǔ)充的未來(lái),各個(gè)業(yè)務(wù)線之間共享彼此的組件模塊料身,想完成一個(gè)新的業(yè)務(wù)或者產(chǎn)品楔壤,只需要像樂(lè)高積木一樣堆砌即可。
三惯驼、構(gòu)建業(yè)務(wù)運(yùn)營(yíng)閉環(huán)
在提供技術(shù)基礎(chǔ)的條件下蹲嚣,我們繼續(xù)思考技術(shù)和業(yè)務(wù)之間的關(guān)系,如何將業(yè)務(wù)價(jià)值最大化祟牲,UI 搭建可以通過(guò)平臺(tái)搭建隙畜,是不是可以把產(chǎn)品運(yùn)營(yíng)同學(xué)也一起參與進(jìn)來(lái),構(gòu)建一個(gè)業(yè)務(wù)運(yùn)營(yíng)閉環(huán)说贝。
1)產(chǎn)品運(yùn)營(yíng)同學(xué)提出需求议惰;
2)研發(fā)人員介入需求開(kāi)發(fā),開(kāi)發(fā)組件乡恕;
3)組件搭建業(yè)務(wù)上線之后言询,一站式追蹤線上業(yè)務(wù)價(jià)值;
4)根據(jù)平臺(tái)的數(shù)據(jù)來(lái)實(shí)時(shí)進(jìn)行運(yùn)營(yíng)策略傲宜,如修改頁(yè)面模塊运杭,下線模塊,添加模塊等等函卒;
5)然后反推產(chǎn)品同學(xué)提出更合理的產(chǎn)品需求辆憔;
如何優(yōu)化鏈路,科學(xué)的運(yùn)營(yíng)體系構(gòu)建運(yùn)營(yíng)業(yè)務(wù)閉環(huán)也是重中之中,并且未來(lái)會(huì)持續(xù)不斷在此方向上探索虱咧。
四熊榛、總結(jié)
FoxPage Native 平臺(tái)上線短短 2 個(gè)月,承載了 30+ 次的業(yè)務(wù)調(diào)整腕巡,5+ 次臨時(shí)調(diào)整埋點(diǎn)的需求玄坦,這是之前做不到的。并且合理系統(tǒng)的設(shè)計(jì)也增加了框架穩(wěn)定性绘沉,上線至今無(wú)任何異常發(fā)生营搅。在保證的高質(zhì)量的交付的同時(shí),也大大減少我們研發(fā)成本梆砸,如一個(gè)復(fù)雜的展示模塊開(kāi)發(fā)转质,從原來(lái)的雙端 4 人日降低到雙端 1 人日。
在首頁(yè)動(dòng)態(tài)化的探索中帖世,遇到了很多的挑戰(zhàn)休蟹,也有很多收獲,后續(xù)有很多的功能和需求還需要繼續(xù)優(yōu)化和完善日矫,并且需要考慮更多的場(chǎng)景支持赂弓。我們相信這是一個(gè)好的開(kāi)頭。