12月10日在SFDC(SegmentFault Developer Conference)大會上初次介紹了手機天貓的Tangram方案,現場時間有限,講得匆忙,特此整理記錄。這篇內容是Tangram的整體介紹與相關業務開發實踐的介紹,后續逐步會將更詳細的方案整理成文分享出來。
什么是Tangram
顧名思義,Tangram中文名是七巧板的意思,我們希望這個框架提供一系列基本單元,就像積木塊一樣,通過快速拼裝就能搭建出一個頁面或者調整頁面的結構。重運營的業務特別是電商業務,往往講究靈活多變,需要對線上業務做實時調整,此類頁面動態化的需求便應運而生。
Tangram的設計理念
對于客戶端開發來說,版本發出去之后,再要修改代碼,是一件成本比較高的事情,針對線上實時調整比較多的地方,往往就采用了H5的方式上線。由于H5的體驗相對Native欠缺一些,就有了后來Facebook的ReactNative(RN),以及阿里自己的解決方案Weex,以Native的方式實現頁面動態調整的能力。在如今,表面上看起來Tangram的方案會有些多余,但是通過了解它的設計與演變,那就知道它還是有存在的理由。
在歷史上,大概兩年前的這個時候,我們團隊接手天貓首頁的業務,迫切需要一套頁面動態化方案。那個時候RN剛剛面世不久,特別是Android版本的RN還不穩定,更不用說后來的Weex了。而我們手里有的一套方案是自己開發過的Dynative(可以理解為初級版本的Weex)。但這些方案有個共同點就是比較重量級,它們都期望從基本的UI元素開始做一套純動態的方案。在種種現有框架不成熟的時候,對于首頁這種重量級的頁面,我們還是希望以一種更加純粹的Native開發模式來支撐業務。
在設計理念上,Tangram也有它的特殊之處,無論是H5還是Weex之類的方案,它們的動態能力在于隨時可發布代碼,它們是面向開發的動態化方案,發布代碼意味著測試、灰度、發布等一系列流程。而Tangram是面向運營和產品的方案,它的動態能力體現在無須做代碼改動,提供足夠多的動態可配置的能力,通過在后臺做樣式的調整來達到頁面調整的目的。所以簡單比較如下:
H5 | Weex/RN | Tangram | |
---|---|---|---|
動態能力 | 強 | 較強 | 偏弱 |
面向人員 | 開發 | 開發 | 運營/產品 |
體量 | 完善的體系 | 較重量級 | 輕量級 |
體驗 | 常規H5體驗 | 鑒于H5和native之間 | 純native體驗 |
鑒于這樣的設計目標,在這個框架里,重點著手于以下四個方面:
- 頁面布局動態化,意思是頁面的排版布局,可以通過后端數據的下發來調整。
- 組件業務化,這里的組件不是指基本的文本、圖片、按鈕等基本UI控件,而是指能承擔一定業務能力的最小復用單元,因此它可能是一個文本和一個圖片的組合這樣子的一種形式。
- 動態能力粗粒度化,通過布局+組件的形式搭建整個頁面,有多少種布局能力是內置在框架里的,有多少組件也是業務接入的時候注冊到框架里的,后端下發的數據聲明了用哪些布局、用哪些組件,通過布局嵌套組件的形式渲染整個頁面。所以這個動態能力比較粗,不像H5或者Weex從基本的UI元素開始搭建整個頁面。
- 組件的復用,為了承載那些個超長頁面,需要對同類型的組件具備回收復用的能力,就像
ListView
、RecyclerView
那樣。
Tangram里重要的概念模型
頁面拆解
從一個實例出發,上圖中展示的是一個早期的天貓首頁,根據導購頁面的特點,我們將頁面拆分成三個層次:頁面——卡片——組件,頁面(第一張圖)指的就是整體可滑動頁面實體,并沒有特殊之處;卡片指的是頁面內可按行劃分的一個一個獨立區塊(參考第二張圖),組件(參考第三張圖)指的是卡片內部一個獨立的、業務級別的單元,它可以是一張圖,也可以是文字+圖的組合。因此整體整個頁面可以這樣描述:一個頁面嵌套了多個卡片,一個卡片嵌套了多個組件。
頁面結構
通過將頁面拆分成三層結構,整個頁面在model上就可以描述成這樣一個樹狀結構。這里最重要的兩個model是卡片和組件的model,整個頁面的動態化將通過它們的動態化以及它們之間組合關系的動態化來完成。下面看這兩層的具體協議描述:
卡片模型
卡片的職責是負責對組件進行布局,那么如何描述布局呢,前面說過,我們采用的是粗粒度的動態化方式,卡片的布局描述就是一種聲明式的方式,因此卡片不需要布局模板,只要在model的數據里描述卡片的類型即可,至于卡片有哪些類型,則是注冊在Tangram框架里的,業務方在接入框架的時候也可以注冊自定義的卡片類型。這樣就讓Tangram省去了對布局模板的解析,簡化了框架復雜度的同時,簡化了開發復雜度。
卡片model描述上有四個組成:header、footer、body、style。最重要的是body部分,它包含了內嵌的組件model,如果卡片沒有body,即沒有組件,也就不在視覺上做渲染。卡片的布局也就是對body里包含的組件來進行布局。Tangram內置了一系列布局能力對組件進行布局,包括流式布局、瀑布流布局、吸頂布局、懸浮布局、輪播布局等等,基本上常見的布局方式都可以覆蓋到。header、footer是卡片的標題和尾部,這是根據業務場景設計的可選內容,因為很多時候一塊業務區域會有個標題之類的東西。在實現的時候,我們可以將他們轉換到body里的組件,但在概念上,單獨描述會更容易理解。style是對布局樣式的描述,所有布局會有一些通用的樣式屬性,也有一些特有的,通過樣式的描述,可以讓布局能力更加豐富。畫圖舉幾個例子:
卡片樣式簡介
這里對卡片的樣式做一些介紹,因為很多時候頁面調整就是對樣式的調整,結構調整也會涉及到樣式調整,因此樣式的動態性對頁面的動態性具有重要貢獻,這里舉例的是幾個通用的樣式屬性,如果卡片比較特殊,還可以自定義樣式屬性。
- backgroundColor: 卡片的背景,在做頁面氛圍的時候經常會用到。
- margin/padding: 卡片外邊距、內邊距,這是通用UI系統都會支持的屬性。
- gap: 卡片內的組件往往需要增加間距,如果通過組件的margin來實現,會有很多不便之處,相鄰組件間左右或者上下都配置了margin,則需要考慮去重的實現,要么就在配置的時候對相鄰組件的margin做精心控制。用gap的概念則很方便,它可以指定水平方向間距、垂直方向間距。
- cols: 默認情況下,流式布局每一列寬度都是等分屏幕寬度的,如果需要做不等分的布局,就可以通過cols來指定每一列的占比,這樣布局能力就能更加豐富了。
組件模型
組件的職責是負責業務邏輯和UI元素展示,它是盡可能小的業務單元,一般以實際設計稿出發,抽象出最小可復用單元。組件也是聲明式的,需要在model的數據里描述組件的類型,至于有哪些類型,也是業務方在接入時預先注冊,因為組件的業務成分比較重,Tangram一般就不內置了。除了類型描述,model數據里剩下的就是組件的數據和樣式描述了。組件的數據不做具體規范,一般滿足組件自身的需求即可,樣式也不做強制規范,但有一些和布局相關的樣式在框架層面會進行支持,這個下文介紹。
在組件的實現上,它首先是一個普通的View,并特殊之處,如果脫離Tangram框架,它也應該能正常運行使用。但在Tangram里,我們為組件設計了一個統一的ViewModel,定義了幾個生命周期事件;通過ViewModel對組件的屬性進行賦值,在組件初始化時會調用init,在滑入屏幕綁定數據時候調用bind,在滑出屏幕解除綁定時調用unbind。
除此之外組件的行為基本上都是業務邏輯了,不做過多介紹,這里再介紹幾個和頁面動態性相關的樣式。
組件樣式簡介
- backgroundColor: 組件的背景,同樣也是在做頁面氛圍的時候經常會用到。
- margin/padding: 組件外邊距、內邊距,同樣也是UI系統都會支持的屬性。
- display: 參考css的設計,特別是在流式布局里,組件默認都是內聯(inline)的,當布局占滿屏幕寬度時,再考慮換行。如果在正常的流式卡片布局里要橫插一行,則可以將組件聲明為block,不然的話,就得將這個卡片打散成三個卡片才行。
- colspan: 默認情況下,流式布局每一列寬度都是等分屏幕寬度的,也就是占用一個格子,組件上聲明colspan可以讓這個組件占用多個格子。它與卡片上的cols區別在于它占用的寬度值是離散的,而cols通過百分比可以做到寬度值的連續分布。
- width/height: 其實是組件的寬高比,用來對組件進行對齊,利于界面排版。
每個組件都可以聲明額外的自定義樣式屬性,比如字體顏色、字體大小等等,這里就不做過多介紹。通過卡片和組件的樣式,基本上就可以組合出大部分場景的頁面結構了,也就是Tangram的初衷——像搭積木一樣拼裝一個頁面。
實現原理
上面介紹了整個Tangram的基本概念,花了這么多篇幅講概念模型,除了告訴大家這個東西是什么、它做什么、它是怎么設計的,最重要傳遞的一個信息是,作為業務系統,需要首先在概念模型上做好架構設計,在協議規范上做好統一,這樣具體的平臺去實現的時候,都能根據這個規范來做實現,不管誰實現的,都屬于Tangram,這就好比JAVA虛擬機規范和JAVA虛擬機的關系一樣。對于我們團隊來說,對Tangram的實現也經歷了一系列變更,但基本規范沒怎么變動,這也是能大規模去支持業務的一個重要支點。下面會介紹目前實現上的思路和重要技術點。
基本結構和流程
主要有這么幾個組成:核心引擎、數據解析器、卡片庫、組件庫、布局框架,核心引擎負責調度整個流程,在啟動框架的時候要核心引擎要做一系列初始化,包括初始化卡片庫和組件庫,也就將內置的卡片類型注冊進框架,將外部業務提供的組件也注冊好,同時也要將數據解析器初始化好,布局框架也要初始化好。當頁面數據傳入的時候,核心引擎調用數據解析器將數據轉換成卡片和組件的model對象,解析過程會根據之前注冊過的卡片、組件類型來解析,不認識的數據將會被拋棄,卡片和組件的基本樣式也會解析。解析完畢的卡片、組件model將會扔給布局框架進行頁面渲染。布局框架根據卡片提供的布局信息進行布局,根據組件提供的組件信息進一步獲取組件實例,貼到布局容器里。
布局框架實現
實現上難度最大的在于布局框架,布局框架的靈活性、性能決定了整個Tangram的靈活性和性能。在Android上,布局框架基于RecyclerView
+自定義LayoutManager
的方式實現;在iOS上,布局框架基于自定義的LazyScrollView來實現。這兩框架基本上都能做到對頁面的扁平化實現,提供了跨卡片的組件級別復用能力。先對這兩塊做一個介紹:
整個頁面樹被解析出卡片+組件的數據列表之后,會對塊數據做進一步轉換。首先提取所有組件model,也就是將組件都打平到同一級別的列表,這個列表會被傳遞給RecyclerView
的Adapter
,因此數據的位置其實就對應了RecyclerView
看到的組件位置。而卡片model,將會拿來構建一個個LayoutHelper
,這些LayoutHelper
是負責具體布局的對象,一種布局類型的卡片對應于一種LayoutHelper
,而且LayoutHelper
還包含了它負責的組件的位置起始區域,它們會被傳遞給自定義的LayoutManager
。當RecyclerView
開始渲染頁面或者滑動時,它內部維護了一個布局狀態,獲取當前屏幕范圍內還有多少區域是空白的,下一個要加載的View
的位置是多少,然后把這些信息告訴LayoutManager
去加載View
做布局。我們的自定義LayoutManager
拿到這個位置之后,就反向查找對應的LayoutHelper
,然后交給LayoutHelper
去布局,這個過程還會涉及到從回收復用池或者通過Adapter
獲取一個組件實例。不同的LayoutHelper
會按照約定的協議進行進一步布局。
對于iOS來說,也有類似的布局邏輯,但這里重點介紹iOS的頁面容器LazyScrollView
。這是一個自定義的滾動布局,具備回收復用能力。它的回收復用算法是這樣的:在頁面渲染前先計算所有組件的位置信息,根據組件在頁面內位置的上邊距做一個排序索引,根據下邊距再做一個排序索引。頁面滾動的時候通過滾動區域與上下邊距的取交集,就可以獲取到當前可見范圍的組件是哪些,然后不可見范圍內的組件實例可以回收,新進入可視區域的組件可以從回收復用池里拿到組件實例或者新創建一個組件實例貼到布局里。
擴展
上面介紹的內容構建里Tangram的基本骨架,但要支撐起業務,還需要很多輔助工具,如果沒有這些擴展,將很難支撐業務。這些擴展有些是內部注冊在框架,也有些是外部注入。
- 點擊處理模塊,組件都需要有點擊交互,點擊處理模塊定義了接口,業務方根據接口實現具體模塊然后注入。
- 曝光處理模塊,與點擊模塊類似,可提供組件曝光時的業務邏輯加載。
- 通用定時器模塊,用來提供計時功能,滿足組件內的倒計時需求、定時需求。
- 事件總線,用來做組件與卡片的通信,或者組件與外部通信等等。
- 腳本動畫,將動畫腳本化,提供動畫的動態能力,讓組件的交互更加豐富。
- 通用請求模塊,有時候卡片數據、或者組件數據需要調用遠程接口更新,同通用請求也是定義了加載接口,外部業務方自行實現注入。
- 純動態組件,解決組件動態問題,因為我們的動態化是粗粒度的,行走江湖免不了內置動態能力滿足不了一個臨時需求的場景。動態組件集成了集團內的動態化方案,目前最主要的就是阿里的Weex方案,通過Weex的動態能力來解決組件的動態能力。
有了這些擴展功能,整個Tangram落地到業務就非常方便了,目前我們支撐了天貓首頁、天貓直播首頁、天貓超市首頁等重要業務,還推廣到了集團其他部門。
實踐經驗
最后分享一些業務開發的經驗,分客戶端和后端運營兩方面介紹。延伸到其他業務,這些經驗應該也是有借鑒意義的。
客戶端
首先是規范與協議的統一,我們這個框架支撐了多個業務,不同業務、不同平臺之間,只有規范統一,才能盡可能支撐多的業務,否則不同業務接入,要做轉換,是一件成本很高的事情。在不同平臺之間統一規范,也可以讓一份數據在多端使用。
在客戶端上開發,穩定性是一個非常重要的指標,整個應用應該有自己的保護模式,對于框架來說,我們也要有防御性編程的思維,特別是是動態化方案,往往根據數據來執行代碼,訪問數據本身要足夠小心,像空字段、類型轉換、數組越界都是常見的問題,通過安全方法的使用,可以在一個地方保證數據訪問的安全性。
組件庫,也是一個非常重要的建設,不同業務之間可以復用相同的組件,減少組件開發。
容器化是實現頁面在線拼裝的一個必要的建設,客戶端需要有一個容器頁面,就像webview一樣,給一個url,就可以加載頁面,后端也也需要做數據的容器化接入,能導入業務數據,按照Tangram協議輸出。再利用組件庫里的現有組件,就可以完成一個頁面搭建,目前有一些簡單的頁面(搜索專輯、推薦專輯)就是這樣完成上線的。
解耦也是一個老生常談的問題,在Tangram里實現擴展能力的時候,解耦這一方面做得就特別棒,像腳本動畫、Weex都是其他團隊的成果,但是可以很方便的插入到Tangram框架里,而且可以熱插拔,業務方不想使用就可以不接入。
運營管理
Tangram是一個動態框架,雖然它的重點技術在客戶端,但是沒有后端的話是不完整的,必須要有一個完善的后端管理平臺來做頁面的日常運維才行。我們開發了一個專門的管理后臺,可以對Tangram頁面做多維度的管理。后端管理平臺還承載了頁面的穩定性、頁面發布的效率、頁面試錯等能力。
Tangram頁面動態調整都是配置發布,視覺調整。因此我們有獨特的發布流程,首先后臺變更完成之后不會直接發布,而是進入到預發布狀態,在這個狀態下,可以通過白名單預覽提前檢查變更效果,預覽的方式是將變更生成一個二維碼,在手機上掃碼預覽,檢查最真實的效果。通過時間機器調整時間,不僅可以預覽這次變更在當前時間的效果,還可以預覽將來某個時間的效果,因為不同的時間點,生效的數據不一樣,因此時間維度的預覽特別有用。另外管理平臺對變更人員也做了權限控制,每個業務方人員只能變更自己負責的業務,不會改動到其他業務的頁面,通過對接流程平臺,讓每次變更都有記錄可查,防止線上數據隨意更改。
有了頁面發布、變更的穩定性保證,發布的效率也是下一個重要考慮的問題。定時發布可以讓變更在指定時間生效,比如雙十一的時候很多東西要0點生效,如果0點做變更、預覽、再上線,風險很大,有了定時發布,可以提前做好準備。一鍵下線的功能,可以做多版本里的某個共用卡片批量下線,特別是緊急情況。批量復制創建卡片和版本通配,都是為了解決新版本發布時候的效率,目前我們針對一個版本客戶端就發布一份頁面配置數據,有了版本通配,可以減少頁面配置的數量,有了批量復制創建,可以在要創建新版本頁面的時候復制頁面。
快速試錯,這是近年來非常熱門的一個領域,當頁面要進行調整的時候,我們希望看到調整的效果,這個時候abtest就派上了用場。天貓有一套自己的試錯平臺,Tangram前后端都對接了這條試錯平臺,在管理平臺可以將變更做成實驗變更,然后導入到試錯平臺下發到客戶端,進入到實驗分桶的用戶就可以訪問到實驗變更,同時試驗平臺在端上也做了數據采集,這樣可以在小范圍內先試驗變更的效果,根據數據來做接下來的決策。