對(duì)于客戶端來說,發(fā)版本身就屬于一種很高成本的行為。然而一個(gè)初創(chuàng)的app,會(huì)有各式各樣的問題,而在初期也不會(huì)像大型app一般有一套成熟的處理異常機(jī)制。而這往往會(huì)造成許多問題,那么問題來了,如何在有限的開發(fā)資源下,做到客戶端的動(dòng)態(tài)化。并且實(shí)現(xiàn)降級(jí)、ABTest等等一系列的行為呢?
What we want?
如前文提到,當(dāng)版本迭代時(shí),首當(dāng)其沖的就是版本的問題。作為團(tuán)隊(duì)的開發(fā),我曾經(jīng)花了很多時(shí)間在思考一個(gè)問題。如何能讓我們的架構(gòu)模型變得優(yōu)雅,變得像橡皮泥一般能隨著業(yè)務(wù)的發(fā)展迅速變形,特別是在一個(gè)創(chuàng)業(yè)公司,迫切需要一個(gè)強(qiáng)大的架構(gòu)體系能支撐業(yè)務(wù)的快速發(fā)展。
那么事情就變得很簡(jiǎn)單了,在維持業(yè)務(wù)發(fā)展速度不變的情況下,盡可能減少發(fā)版的頻率,以技術(shù)的方式在不發(fā)版的情況下滿足“偽發(fā)版”。
當(dāng)然,還有其他的一些想法,會(huì)在后文提到:
- ABTest
- 灰度
- 降級(jí)策略
- 動(dòng)態(tài)調(diào)用本地服務(wù)
What to do?
有了想法知道,思路就比較清晰了,會(huì)針對(duì)性的對(duì)目的進(jìn)行處理。
“偽發(fā)版”
對(duì)于app來說,偽發(fā)版的方案已經(jīng)在業(yè)內(nèi)比較成熟了。我們可以用weex/react native(以同一JS代碼,在H5、iOS、Android同時(shí)實(shí)現(xiàn)同一頁(yè)面)的方式來實(shí)現(xiàn)在不發(fā)版的情況下實(shí)現(xiàn)頁(yè)面級(jí)別的更新。
我們?cè)囅胍幌拢m然weex實(shí)現(xiàn)了頁(yè)面的動(dòng)態(tài)更新。但是卻沒法很優(yōu)雅地處理新頁(yè)面與老頁(yè)面的跳轉(zhuǎn)關(guān)系。打個(gè)比方,一個(gè)native頁(yè)面A已經(jīng)在代碼里寫死點(diǎn)擊按鈕跳轉(zhuǎn)頁(yè)面B,此時(shí)就算用weex/rn寫了一個(gè)新頁(yè)面,也是很難跳轉(zhuǎn)的(當(dāng)然如果兩個(gè)頁(yè)面都是weex/rn寫的,就不會(huì)存在這樣的問題,但是在大多數(shù)應(yīng)用場(chǎng)景里,還是會(huì)存在部分weex/rn,部分native的頁(yè)面)。
即 A -> B 是一個(gè)既定的關(guān)系。為了達(dá)到動(dòng)態(tài)化,必然需要對(duì)整個(gè)app的導(dǎo)航進(jìn)行統(tǒng)一處理。
這就是URLRoute要做的事情!
正如其名,URLRoute會(huì)有一個(gè)路由規(guī)則。
URLRoute
而動(dòng)態(tài)化的秘訣就是將無法變動(dòng)的邏輯代碼維護(hù)成文本。即 代碼 -> 文本 。
文本沒有編譯、簽名等等發(fā)版需要的步驟,它的更新完全是可以由文件的更新完成,而這恰恰是不需要發(fā)版的。
拿iOS為例,我實(shí)現(xiàn)了一個(gè)URLRoute的庫(kù),其中的處理流程可以由下圖所示:
其中URL與對(duì)應(yīng)的class的映射是由一個(gè)文件維護(hù)的,如圖所示:
通過維護(hù)一個(gè)dictionary(map)維護(hù)URL與本地頁(yè)面或服務(wù)的關(guān)系。
ABTest && 灰度
任何業(yè)務(wù)在發(fā)展的階段都不可避免會(huì)產(chǎn)生分歧,此時(shí)需要如果業(yè)務(wù)架構(gòu)支持對(duì)不同的人群命中不同粒度的產(chǎn)品,并且給出數(shù)據(jù)以展示哪一種結(jié)果更好,這對(duì)于思考產(chǎn)品的人有更多的選擇余地。
而通過URLRoute,我們完全可以針對(duì)不同的人群派發(fā)不同的映射文件達(dá)到ABTest的目的。
甚至在某一個(gè)版本上可以做到灰度,10%->20%->...->100%。
降級(jí)策略
試想一下,如果以上線的項(xiàng)目產(chǎn)生了重大BUG怎么辦。對(duì)于iOS來說,可以通過Runtime的方式達(dá)到patch。但是對(duì)于Android來說,現(xiàn)有的三種方案:classloader,更改jar包,更改apk。后兩者更不用說,我們并不是一個(gè)apk。而前者在現(xiàn)如今的動(dòng)態(tài)方案中逐漸被拋棄。其實(shí)都不適合我們,這些其實(shí)都需要一套完整的解決方案,并不僅僅是導(dǎo)入庫(kù)就行,還要考慮與server的交互等。
那么如何做到一個(gè)方式,能以最簡(jiǎn)單的方式做到在產(chǎn)生BUG時(shí),以最小的成本并且在兩端以同樣的策略去做到修復(fù)或者降級(jí)呢?
答案是我們可以通過更改映射文件,將原先有問題的URL對(duì)應(yīng)的value替換掉,即A->B變成A->C(C可以是一個(gè)error頁(yè)面,也可以是一個(gè)weex/rn頁(yè)面)。當(dāng)然你會(huì)問如果本地沒有實(shí)現(xiàn)C怎么辦?沒關(guān)系,我們刪掉那條URL記錄,當(dāng)URLRoute攔截不到URL時(shí),會(huì)以webview的形式打開,這樣就降級(jí)成H5了。
動(dòng)態(tài)調(diào)用本地服務(wù)
URLRoute不僅僅處理了頁(yè)面之間的跳轉(zhuǎn)邏輯,也可以處理service。這里的service可以理解為任何native的行為,比如登錄、強(qiáng)制更新、發(fā)網(wǎng)絡(luò)請(qǐng)求等等。
我們可以事先注冊(cè)好對(duì)應(yīng)的module,通過URL調(diào)用對(duì)應(yīng)的service。
這是很重要的,比如我們產(chǎn)生了重大BUG,我們就可以強(qiáng)制登出用戶,甚至強(qiáng)制所有頁(yè)面都跳到error頁(yè)面等等。
How to do?
我們可以在要實(shí)現(xiàn)URLRoute的class,在plist文件里聲明,key為對(duì)應(yīng)的host + path,value為對(duì)應(yīng)的class name。
// 默認(rèn)調(diào)用方式
OpenURL(@"http://m.kuailejim.com/home?id=552131");
// 如果需要傳URL不能表達(dá)的參數(shù)(Object參數(shù))
OpenURLWithParams(@"http://m.kuailejim.com/home?id=552131", paramsDictionary);
這里的scheme為http,是因?yàn)槲以赨RLRoute里做了處理,當(dāng)這里的key(m.kuailejim.com/home)沒有被攔截到即在plist文件中并沒有找到對(duì)應(yīng)的class來處理時(shí),我會(huì)判斷scheme是否為http/https,如果是,則會(huì)跳到H5以實(shí)現(xiàn)降級(jí)。
我們知道,URL傳參對(duì)于object類型是不友好的,于是需要實(shí)現(xiàn)一個(gè)能帶dictionary(map)的方法,并且在URLRoute內(nèi)部實(shí)現(xiàn)基于key/value形式參數(shù)的解析。
對(duì)應(yīng)的class需要實(shí)現(xiàn)下述方法,這樣URLRoute通過runtime動(dòng)態(tài)創(chuàng)建對(duì)應(yīng)的class,并且調(diào)用class已經(jīng)實(shí)現(xiàn)的代理方法達(dá)到處理的效果:
- (id)initWithUrlRequest:(URLRouteRequest *)request;
這里還需要注意,在URLRoute里要hook對(duì)應(yīng)的viewdidappear方法來拿到當(dāng)前的view controller,才能做到push和model的操作。如果是model操作還需實(shí)現(xiàn)一個(gè)navigation controller(因?yàn)橛锌赡軙?huì)在新的頁(yè)面里push)
AOP統(tǒng)一處理,例如想在跳轉(zhuǎn)時(shí)對(duì)所有url記錄埋點(diǎn)等功能,這個(gè)很好理解,只要在URLRoute里在實(shí)現(xiàn)攔截前統(tǒng)一做處理就行。
總結(jié)
這只是客戶端動(dòng)態(tài)化的第一步,baby steps to big dreams。