早在 2010 年 58 同城誕生第一版 iOS 客戶端,按照傳統(tǒng)的 MVC 模式去設(shè)計(jì),純 Native 頁(yè)面,這時(shí)的功能較為簡(jiǎn)單,架構(gòu)也是如此,從上至下分為 UI 展現(xiàn)、業(yè)務(wù)邏輯、數(shù)據(jù)訪問三層,如圖 1 所示。和同期其他公司一樣,App 的出發(fā)點(diǎn)是為了快速搶占市場(chǎng),采取“短平快”的方式開發(fā)。純 Native 的 App 在早期業(yè)務(wù)量不是太大的情況下,能滿足業(yè)務(wù)的需求。
由于蘋果審核周期較長(zhǎng),業(yè)務(wù)需求不斷增大,有些業(yè)務(wù)如果用 Native 進(jìn)行開發(fā),工作量大投入人員較多,也不能動(dòng)態(tài)更新,如 58 App 的大類、列表、詳情頁(yè)面。這種情況下,用 HTML5 是比較流行的解決方式,由此產(chǎn)生了第二版架構(gòu),如圖 2 所示,在 UI 層添加了 HTML5 頁(yè)面及 Hybrid 交互框架。
當(dāng)時(shí) 58 App 設(shè)計(jì)時(shí)用于加載 HTML5 的組件是 UIWebView,也只能使用這個(gè)(彼時(shí)還沒有 WKWebView),但實(shí)現(xiàn)起來有幾個(gè)問題是需要解決的:
怎么解決 Hybrid 中 Web 和 Native 交互問題,如用戶點(diǎn)擊一個(gè)類別,能調(diào)起 Native 的一些方法去執(zhí)行相關(guān)頁(yè)面跳轉(zhuǎn)或?qū)懭罩尽?/div>
如何提高 HTML5 頁(yè)面的加載速度,HTML5 頁(yè)面加載時(shí)要下載一些 JavaScript、CSS 及圖片資源,是比較耗時(shí)的。
設(shè)置緩存
為了方便描述,本文先介紹如何提高 HTML5 頁(yè)面加載速度的問題。
對(duì)于一些訪問比較頻繁的頁(yè)面,如大類列表詳情,我們?cè)缙诓捎玫亩际?HTML5 頁(yè)面。要加速這些頁(yè)面的渲染,就要想辦法提升資源的加載。那么如何實(shí)現(xiàn)呢?首先想到的是使用緩存,我們可以把這些頁(yè)面的資源內(nèi)置到 App 中隨版本發(fā)布。
由于 UIWebView 在發(fā)請(qǐng)求的時(shí)候都會(huì)走 NSURLCache 的這個(gè)方法:
- (nullable NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest *)request;
我們可以從 NSURLCache 派生出子類 WBHybrid Component,復(fù)寫 cachedResponseForRequest:方法,在這之中加載 App 的內(nèi)置資源,具體加載策略可見圖 3。
圖3 緩存處理流程
其中,H5ViewController 為 HTML5 載體頁(yè)面,WBCacheHandler 為專門處理內(nèi)置資源類,用于加載、查找、下載、保存內(nèi)置資源。URL 的 query 中設(shè)置版本號(hào)參數(shù) cachevers 作為資源緩存的標(biāo)識(shí),其值為數(shù)字類型,假設(shè) cachev1,其與內(nèi)置資源中的版本號(hào)如為 cachev2 進(jìn)行對(duì)比,若 cachev2>= cachev1,表示內(nèi)置資源中是最新數(shù)據(jù),直接給請(qǐng)求返回?cái)?shù)據(jù);否則下載新的內(nèi)置資源,同時(shí)根據(jù) cachev1- cachev2 的差值進(jìn)行判斷,如設(shè)置一個(gè)臨界值 x,若差值大于 x,則說明內(nèi)置資源為舊,給請(qǐng)求返回 nil,否則返回內(nèi)置數(shù)據(jù),讓請(qǐng)求先用緩存數(shù)據(jù),下次啟動(dòng)時(shí)再用新數(shù)據(jù)。
內(nèi)置數(shù)據(jù)采用的是一個(gè) bundle 包,如圖 4 所示,CacheResources.bundle 為內(nèi)置包名,里面包含了一個(gè)索引文件和若干個(gè)內(nèi)置數(shù)據(jù)文件,其中索引文件中每項(xiàng) item 格式為 key、版本號(hào)和文件名。
圖4 緩存包結(jié)構(gòu)
想要使用自定義的 NSURLCache,必須在 App 啟動(dòng)時(shí)初始化 WBHybridComponent,并進(jìn)行設(shè)置,替換默認(rèn)的 Cache,注意:這個(gè)設(shè)置必須在所有請(qǐng)求之前進(jìn)行,否則設(shè)置失效,而是采用默認(rèn)的 NSURLCache 實(shí)例,我們?cè)?jīng)踩過這個(gè)坑。
基于 AJAX 的 Hybrid 框架
對(duì)于前面所列的第一個(gè)問題,我們是要設(shè)計(jì)一個(gè) Web/Native 的 Hybrid 框架。交互主要包括兩部分內(nèi)容,一是 Native 調(diào)用 Web,這個(gè)比較簡(jiǎn)單,直接通過 UIWebView 的 stringByEvaluatingJavaScriptFromString:執(zhí)行一段 JS 腳本,并返回執(zhí)行結(jié)果,本文主要分享 Web 調(diào) Native 的方法。
對(duì)于 Web 調(diào) Native 交互的方式,我們采用異步 AJAX 進(jìn)行,創(chuàng)建一個(gè) XMLHttpRequest 對(duì)象,執(zhí)行 send()進(jìn)行異步請(qǐng)求,Native 攔截。
由于 XMLHttpRequest 的方式是進(jìn)行頁(yè)面局部刷新,并不能被 UIWebViewDelegate 代理的 - (BOOL)webView:(UIWebView *)webView shouldStartL
OAdWithRequest:(NSURLRequest *)request navigationType:
(UIWebViewNavigationType)navigationType 方法攔截到,設(shè)計(jì)到這里又出現(xiàn)了新問題,如何讓 Native 能攔截到 AJAX 請(qǐng)求呢?
經(jīng)過一番調(diào)研,我們找到了用于緩存的 NSURLCache,對(duì)于 UIWebView 中的所有請(qǐng)求(包括 AJAX 請(qǐng)求)都會(huì)走 NSURLCache。因此,我們決定采用復(fù)用緩存中的 WBHybridComponent 攔截 AJAX 請(qǐng)求,具體 Web 調(diào) Native 的交互設(shè)計(jì)如圖 5 所示。
圖5 Hybrid 框架處理流程圖
其中,H5ViewController 為 HTML5 的載體頁(yè),WBWebView 是 UIWebView 派生類。WBWebView 中通過 AJAX 發(fā)出的異步請(qǐng)求,在 WBHybridComponent 中被攔截,再通過 WBHybridJSHandler 中的 dic 表找到對(duì)應(yīng)的 WBActionAnalysis 對(duì)象,然后在 WBActionAnalysis 中分析異步請(qǐng)求傳過來的協(xié)議,取出 action 字段,再根據(jù) action 值找到 delegate 即 H5ViewController 中對(duì)應(yīng)的方法。
AJAX 發(fā)出的請(qǐng)求我們約定為:nativechannel://?paras=<json 協(xié)議>,WBHybridComponent 在攔截時(shí)判斷 URL 中是否為 nativechannel 的協(xié)議頭,如果是則為 Web 調(diào)起 Native 操作,需要進(jìn)行后續(xù) Native 處理;否則放過進(jìn)行其他處理。<json 協(xié)議> 的簡(jiǎn)化格式如圖 6 所示,這是二手車大類頁(yè)點(diǎn)擊二手車類目 Web 調(diào) Native 時(shí) AJAX 傳過來的協(xié)議。
圖6 Web 調(diào) Native 傳輸協(xié)議
改進(jìn)的 Hybrid 框架
前面我們?cè)O(shè)計(jì)的 Hybrid 框架,通過創(chuàng)建 XMLHttpRequest 對(duì)象發(fā)送 AJAX 請(qǐng)求的方式能達(dá)到 Web 調(diào) Native 的目的,也可以滿足業(yè)務(wù)上的需求,在一段內(nèi)發(fā)揮了重要作用。但隨著時(shí)間的推移,這個(gè) Hybrid 框架暴露出了一些問題,如下所示。
1.我們發(fā)現(xiàn) App 中存在大量的內(nèi)存泄露,經(jīng)查罪魁禍?zhǔn)拙故?UIWebView。調(diào)研發(fā)現(xiàn) UIWebView 中執(zhí)行 XMLHttpRequest 異步請(qǐng)求時(shí)會(huì)有內(nèi)存泄露,網(wǎng)上也有人探討過這個(gè)問題,參考博文:http://blog.techno-barje.fr//post/2010/10/04/UIWebView-secrets-part1-memory-leaks-on-xmlhttprequest/。
2.Hybrid 交互方式與緩存都使用 NSURLCache 的派生類 WBHybridComponent 執(zhí)行攔截,其初衷也是用于緩存。我們的 Hybrid 框架將兩者耦合在一起,這對(duì)于后期的開發(fā)和性能優(yōu)化工作會(huì)帶來不少隱患。
3.我們?cè)?Hybrid 交互的時(shí)候維護(hù)了一個(gè)
由于 iframe 方式是整個(gè)頁(yè)面刷新,所以能執(zhí)行 UIWebViewDelegate 的回調(diào)方法 - (BOOL)webView:(UIWebView )webView shouldStartLoadWithRequest:(NSURLRequest )request navigationType:(UIWebViewNavigationType)navigationType。我們可以直接在這個(gè)方法中攔截 Web 的調(diào)起,iframe 方式處理流程如圖 7 所示。
圖7 iframe 的 Hybrid 交互方式
通過 iframe 的方式,我們 App 極大地簡(jiǎn)化了 Hybrid 框架的交互流程,同時(shí)也解決了內(nèi)存泄露、與緩存功能耦合、消耗不必要的內(nèi)存空間等問題。
第三個(gè)版本架構(gòu)
隨著業(yè)務(wù)的進(jìn)行,一些新的技術(shù)需求來了,比如有些基礎(chǔ)模塊可以從 App 中獨(dú)立出來進(jìn)行多應(yīng)用間的復(fù)用;需要為轉(zhuǎn)轉(zhuǎn) App 提供一個(gè)日志 SDK;為違章查詢等 App 提供登錄的 Passport SDK;為其他 App 提供一個(gè)可定制化的分享組件等等。
App 拆分組件
這時(shí)我們迫切地需要在工程代碼層面對(duì)原來的 App 進(jìn)行拆分、組件化開發(fā),如圖 8 所示。
圖8 第三版架構(gòu)
我們將 App 拆分成三層,從下至上依次是基礎(chǔ)服務(wù)層、基礎(chǔ)業(yè)務(wù)層、主業(yè)務(wù)層:
1.基礎(chǔ)服務(wù)層里的組件是與業(yè)務(wù)無關(guān)的,供上層調(diào)用,每個(gè)組件為一個(gè)工程,如網(wǎng)絡(luò)、數(shù)據(jù)庫(kù)、日志等。這里面有些組件是整個(gè)公司的其他 App 也在使用,如樂高日志,我們對(duì)外提供一個(gè) SDK,與文檔一起放在代碼服務(wù)器上供其他團(tuán)隊(duì)使用。并將 58 App 中用到的所有第三方庫(kù)都集中起來存放到一個(gè)專門的工程中,也便于更新維護(hù)。
2.基礎(chǔ)業(yè)務(wù)層里的組件是與業(yè)務(wù)相關(guān)的,供主業(yè)務(wù)層使用,每個(gè)組件是一個(gè)工程,如登錄、分享、推送、IM 等,我們把 Hybrid 框架也歸在業(yè)務(wù)層。其中登錄組件我們做成 Passport SDK,供公司其他 App 集成調(diào)用。
3.主業(yè)務(wù)包括 App 首頁(yè)、個(gè)人中心、各業(yè)務(wù)線業(yè)務(wù)和第三方接入業(yè)務(wù),業(yè)務(wù)線業(yè)務(wù)主要包括發(fā)布、大類、列表、詳情。
集成管理組件
工程拆分完后,就是工程集成了,我們用 Cocoapods 將各工程集成到一起編譯運(yùn)行和打包,對(duì)于每一個(gè)工程配置好.podspec 文件。在配置 podfile 文件時(shí),當(dāng)用于本地開發(fā)時(shí),我們通過 path 的方式進(jìn)行集成,不用臨時(shí)下載工程代碼,如下所示。
pod proj, :path => '~/58_ios_libs/proj’
在進(jìn)行 Jenkins 打包時(shí),我們通過 Git 方式將代碼實(shí)時(shí)下載:
pod proj, :git => 'git@gitlab.58corp.com:58_ios_team/proj.git',:branch => '1.0.0'。
GitLab 服務(wù)進(jìn)行代碼管理
我們?cè)诰钟蚓W(wǎng)搭建一個(gè) GitLab 服務(wù),用于管理所有工程代碼,并設(shè)置好開發(fā)組及相應(yīng)的權(quán)限。通過 GitLab 還可以實(shí)現(xiàn)提交代碼審核、代碼合并請(qǐng)求及工程分支保護(hù)。
第四版架構(gòu)
隨著 58 App 用戶量的劇增,各業(yè)務(wù)線業(yè)務(wù)迅速增長(zhǎng),對(duì) 58 App 又提出了新需求,如為加快大類列表詳情頁(yè)面的渲染速度,需要將原來這些 HTML5 頁(yè)面 Native 化;再如各業(yè)務(wù)線要定制列表詳情和篩選樣式。面對(duì)如此眾多需求,顯然原來的架構(gòu)已經(jīng)滿足不了,那就需要我們進(jìn)一步改進(jìn)客戶端架構(gòu),將主業(yè)務(wù)層進(jìn)一步拆分。
主業(yè)務(wù)層拆分
我們對(duì)主業(yè)務(wù)層進(jìn)行一個(gè)拆分,拆分后的整體架構(gòu)如圖 9 所示,其中每一個(gè)模塊為一個(gè)工程,也是一個(gè)組件。
圖9 第四版架構(gòu)
我們將首頁(yè)、發(fā)布、發(fā)現(xiàn)、消息中心、個(gè)人中心及第三方業(yè)務(wù)等都從主業(yè)務(wù)層拆分出來成為獨(dú)立工程。同樣將房產(chǎn)、二手、二手車、黃頁(yè)、招聘等業(yè)務(wù)線的代碼從原工程里面剝離出來,每個(gè)業(yè)務(wù)線獨(dú)立一工程,將列表和詳情分別剝離出來并進(jìn)行 Native 化,為上層業(yè)務(wù)線定制功能提供接口。
業(yè)務(wù)線拆分的時(shí)候我們遵循以下幾個(gè)原則:
1.各業(yè)務(wù)線之間不能有依賴關(guān)系,因?yàn)槲覀兊臉I(yè)務(wù)線在開發(fā)的整個(gè)過程中都是獨(dú)立運(yùn)行的,不會(huì)含有其他業(yè)務(wù)線代碼。
2.非業(yè)務(wù)線工程不能對(duì)各業(yè)務(wù)線有依賴關(guān)系,即所有業(yè)務(wù)線都不集成進(jìn) App 也要能正常編譯。
3.各業(yè)務(wù)線對(duì)非業(yè)務(wù)線工程可以保留必要的依賴,如業(yè)務(wù)線對(duì)列表組件的依賴。
在拆分過程中我們也采取了一些策略,如在拆分招聘業(yè)務(wù)線時(shí),先把招聘業(yè)務(wù)線從集成后的工程中刪除,進(jìn)行編譯,會(huì)出現(xiàn)各種編譯錯(cuò)誤,說明是有工程對(duì)招聘業(yè)務(wù)線代碼進(jìn)行依賴。如何解決這些依賴關(guān)系呢?我們主要是解決相互依賴關(guān)系,招聘業(yè)務(wù)線對(duì)非業(yè)務(wù)線工程肯定是有一定的依賴關(guān)系,這個(gè)先保留,我們要解決的是其他組件甚至可能是其他業(yè)務(wù)線對(duì)招聘的依賴。我們總結(jié)了下,主要用了以下幾種方式:
1.將依賴的文件或方法下沉,如有些文件并不是招聘業(yè)務(wù)線專用的,可以從招聘中下沉到其他工程,同樣有些方法也可以下沉。
2.Runtime,這種方式比較普遍,但也不需要所有地方都用,畢竟其維護(hù)成本還是比較高的。
3.Category 方式,如個(gè)人中心組件中方法 funA 要調(diào)用招聘組件中的方法 funB,但 funB 的實(shí)現(xiàn)是要依賴招聘內(nèi)部代碼,這種情況下個(gè)人中心是依賴招聘業(yè)務(wù)線的,理論上招聘可以依賴個(gè)人中心,而不應(yīng)該反過來依賴。解決辦法是可以在個(gè)人中心添加一個(gè)類,如 ClassA,里面添加方法 funB,但實(shí)現(xiàn)為空,如果帶返回值可以返回一個(gè)默認(rèn)值,再在招聘中添加一個(gè) ClassA 的類別 ClassA+XX,將原來招聘中的方法 funB 放入 ClassA+XX,這樣如果招聘集成進(jìn)來,就會(huì)執(zhí)行 ClassA+XX 中的 funB 方法,否則執(zhí)行個(gè)人中心自己的 funB 方法。
跳轉(zhuǎn)總線
總線包括 UI 總線和服務(wù)總線,前者主要處理組件間頁(yè)面間的跳轉(zhuǎn),尤其是在主業(yè)務(wù)層,UI 總線用得比較頻繁。服務(wù)總線主要處理組件間的服務(wù)調(diào)用,這里主要講跳轉(zhuǎn)總線。在主業(yè)務(wù)層,被封裝成的各個(gè)組件需要通過 UI 總線進(jìn)行頁(yè)面跳轉(zhuǎn),我們?cè)O(shè)計(jì)了一個(gè)總分發(fā)中心和子分發(fā)中心的模式進(jìn)行處理,如圖 10 所示。
圖10 UI 跳轉(zhuǎn)總線
主業(yè)務(wù)層每個(gè)組件內(nèi)都有一個(gè)子分發(fā)中心,它的處理邏輯由各組件內(nèi)來進(jìn)行,但必須實(shí)現(xiàn)一些共同的接口,且這個(gè)子分發(fā)中心需要進(jìn)行注冊(cè)。當(dāng)組件內(nèi)需要進(jìn)行 UI 跳轉(zhuǎn)時(shí),調(diào)用總分發(fā)中心,將跳轉(zhuǎn)協(xié)議傳入總分發(fā)中心,總分發(fā)中心根據(jù)協(xié)議中組件標(biāo)識(shí)(如業(yè)務(wù)線標(biāo)識(shí))找到對(duì)應(yīng)的目標(biāo)組件子分發(fā)中心,將跳轉(zhuǎn)協(xié)議透?jìng)鞯綄?duì)應(yīng)的子分發(fā)中心。接下來的跳轉(zhuǎn)由子分發(fā)中心去完成。這樣的方式極大降低了組件間的耦合度。
UI 總線中的跳轉(zhuǎn)協(xié)議我們?cè)瓉碛?JSON 形式,后來統(tǒng)一調(diào)整為 URL 的方式,將 m 調(diào)起、瀏覽器調(diào)起、push 調(diào)起、外部 App 調(diào)起和 App 內(nèi)跳轉(zhuǎn)統(tǒng)一處理。
新統(tǒng)跳協(xié)議 URL 格式如下:
wbmain://jump/job/list? ABMark=markID¶ms=
其中,wbmain 為 58 App 的 scheme,job 為招聘業(yè)務(wù)線標(biāo)識(shí),list 為到列表頁(yè),ABMark 為 AB 測(cè)跳轉(zhuǎn)用的標(biāo)識(shí) ID,后面會(huì)細(xì)講,params 為傳過來的一些參數(shù),如是否需要?jiǎng)赢嫞琾ush 還 present 方式入棧等。為了兼容老協(xié)議,我們將原來協(xié)議中的一部分內(nèi)容直接透?jìng)鞯?params 中。
AB 測(cè)跳轉(zhuǎn)
對(duì)于指定跳轉(zhuǎn) URL,有時(shí)跳轉(zhuǎn)的目標(biāo)頁(yè)面是不固定的,如我們的發(fā)布頁(yè)面,有 HTML5 和 React Native 兩套頁(yè)面,如果 React Native 頁(yè)面出了問題,可以將 URL 做修改跳到 HTML5 頁(yè)面。具體方案是服務(wù)器下發(fā)一個(gè)路由表,每個(gè)表項(xiàng)有一個(gè) ID 和對(duì)應(yīng)新的跳轉(zhuǎn) URL,每個(gè)表項(xiàng)設(shè)置有過期時(shí)間。跳轉(zhuǎn)的 URL 可以帶有 AB 測(cè)跳轉(zhuǎn)用的標(biāo)識(shí) ID,即 markID。如果有這個(gè)標(biāo)識(shí),跳轉(zhuǎn)時(shí)就去與路由表中的表項(xiàng)匹配,如果命中就改用路由表中的 URL 跳轉(zhuǎn),否則還用原來的 URL 執(zhí)行跳轉(zhuǎn),大概流程如圖 11 所示。
圖11 AB 測(cè)跳轉(zhuǎn)流程圖
靜態(tài)庫(kù)方案
為了提高整個(gè) App 的編譯速度,我們?yōu)槊總(gè)工程配置一個(gè)對(duì)應(yīng)的庫(kù)工程,里面預(yù)先由源碼工程編譯出來一個(gè)對(duì)應(yīng)的靜態(tài)庫(kù),如圖 12 所示。
圖12 源碼庫(kù)與靜態(tài)庫(kù)對(duì)應(yīng)關(guān)系
開發(fā)人員可以將權(quán)限內(nèi)的源碼和靜態(tài)下載到本地,按需進(jìn)行源碼和庫(kù)混合集成,如對(duì)于招聘業(yè)務(wù)線 RD,我們只需關(guān)心招聘業(yè)務(wù)線源碼工程,不需要其他業(yè)務(wù)線的源碼或靜態(tài)庫(kù),剩下的工程可以選擇全部用靜態(tài)庫(kù)進(jìn)行集成。
對(duì)于 Jenkins 打包平臺(tái),我們也可以根據(jù)需求適當(dāng)在源碼和靜態(tài)庫(kù)之間做選擇。對(duì)于一些特殊的工程,如第三方庫(kù)工程 ThirdComponent,一般也不會(huì)變,可以直接接入對(duì)應(yīng)的靜態(tài)庫(kù)工程 ThirdComponentLib。
總結(jié)
業(yè)務(wù)在不斷變化,需求持續(xù)增多,技術(shù)也在不斷地更新,我們的架構(gòu)也需要不斷進(jìn)行調(diào)整和升級(jí),架構(gòu)的演進(jìn)是一項(xiàng)長(zhǎng)期的任務(wù)。
核心關(guān)注:拓步ERP系統(tǒng)平臺(tái)是覆蓋了眾多的業(yè)務(wù)領(lǐng)域、行業(yè)應(yīng)用,蘊(yùn)涵了豐富的ERP管理思想,集成了ERP軟件業(yè)務(wù)管理理念,功能涉及供應(yīng)鏈、成本、制造、CRM、HR等眾多業(yè)務(wù)領(lǐng)域的管理,全面涵蓋了企業(yè)關(guān)注ERP管理系統(tǒng)的核心領(lǐng)域,是眾多中小企業(yè)信息化建設(shè)首選的ERP管理軟件信賴品牌。
轉(zhuǎn)載請(qǐng)注明出處:拓步ERP資訊網(wǎng)http://www.ezxoed.cn/
本文標(biāo)題:58 同城 iOS 客戶端組件化演變歷程
本文網(wǎng)址:http://www.ezxoed.cn/html/consultation/10839320604.html