淺談Hybrid技術的設計與實現第三彈——落地篇

前言

接上文:(閱讀本文前,建議閱讀前兩篇文章先)

淺談Hybrid技術的設計與實現

淺談Hybrid技術的設計與實現第二彈

根據之前的介紹,大家對前端與Native的交互應該有一些簡單的認識了,很多朋友就會覺得這個交互很簡單嘛,其實并不難嘛,事實上單從Native與前端的交互來說就那點東西,真心沒有太多可說的,但要真正做一個完整的Hybrid項目卻不容易,要考慮的東西就比較多了,單從這個交互協(xié)議就有:

① URL Schema

② JavaScriptCore

兩種,到底選擇哪種方式,每種方式有什么優(yōu)勢,都是我們需要深度挖掘的,而除此之外,一個Hybrid項目還應該具有以下特性:

① 擴展性好——依靠好的約定

② 開發(fā)效率高——依賴公共業(yè)務

③ 交互體驗好——需要解決各種兼容問題

我們在實際工作中如何落地一個Hybrid項目,如何推動一個項目的進行,這是本次我們要討論的,也希望對各位有用。

文中是我個人的一些開發(fā)經驗,希望對各位有用,也希望各位多多支持討論,指出文中不足以及提出您的一些建議。

設計類博客

http://www.cnblogs.com/yexiaochai/p/4921635.html
http://www.cnblogs.com/yexiaochai/p/5524783.html

iOS博客

http://www.cnblogs.com/nildog/p/5536081.html#3440931

Android博客

https://home.cnblogs.com/u/vanezkw

代碼地址:https://github.com/yexiaochai/Hybrid

因為IOS不能掃碼下載了,大家自己下載下來用模擬器看吧,下面開始今天的內容。

總體概述在第一章,有興趣大家去看

細節(jié)設計在第二章,有興趣大家去看

本章主要為打補丁

邊界問題

在我們使用Hybrid技術前要注意一個邊界問題,什么項目適合Hybrid什么項目不適合,這個要搞清楚,適合Hybrid的項目為:

① 有60%以上的業(yè)務為H5

② 對更新(開發(fā)效率)有一定要求的APP

不適合使用Hybrid技術的項目有以下特點:

① 只有20%不到的業(yè)務使用H5做

② 交互效果要求較高(動畫多)

任何技術都有適用的場景,千萬不要妄想推翻已有APP的業(yè)務用H5去替代,最后會證明那是自討苦吃,當然如果僅僅想在APP里面嵌入新的實驗性業(yè)務,這個是沒問題的。

交互約定

根據之前的學習,我們知道與Native交互有兩種交互:

① URL Schema

② JavaScriptCore

而兩種方式在使用上各有利弊,首先來說URL Schema是比較穩(wěn)定而成熟的,如果使用上文中提到的“ajax”交互方式,會比較靈活;而從設計的角度來說JavaScriptCore似乎更加合理,但是我們在實際使用中卻發(fā)現,注入的時機得不到保障。

iOS同事在實體JavaScriptCore注入時,我們的原意是在webview載入前就注入所有的Native能力,而實際情況是頁面js已經執(zhí)行完了才被注入,這里會導致Hybrid交互失效,如果你看到某個Hybrid平臺,突然header顯示不正確了,就可能是這個問題導致,所以JavaScriptCore就被我們棄用了。

JavaScriptCore可能導致的問題:
① 注入時機不唯一(也許是BUG)
② 刷新頁面的時候,JavaScriptCore的注入在不同機型表現不一致,有些就根本不注入了,所以全部hybrid交互失效

如果非要使用JavaScriptCore,為了解決這一問題,我們做了一個兼容,用URL Schema的方式,在頁面邏輯載入之初執(zhí)行一個命令,將native的一些方式重新載入,比如:

1 _.requestHybrid({ 2 tagname: 'injection' 3 });

這個能解決一些問題,但是有些初始化就馬上要用到的方法可能就無力了,比如:

① 想要獲取native給予的地理信息

② 想要獲取native給予的用戶信息(直接以變量的方式獲?。?

作為生產來講,我們還是求穩(wěn),所以最終選擇了URL Schema。

明白了基本的邊界問題,選取了底層的交互方式,就可以開始進行初步的Hybrid設計了,但是這離一個可用于生產,可離落地的Hybrid方案還比較遠。

賬號體系

一般來說,一個公司的賬號體系健壯靈活程度會很大程度反映出這個研發(fā)團隊的整體實力:

① 統(tǒng)一的鑒權認證

② 短信服務圖形驗證碼的處理

③ 子系統(tǒng)的權限設計、公共的用戶信息導出

④ 第三方接入方案

⑤ 接入文檔輸出

⑥ ......

這個技術方案,有沒有是一回事(說明沒思維),有幾套是一回事(說明比較亂,技術不統(tǒng)一),對外的一套做到了什么程度又是一回事,當然這個不是我們討論的重點,而賬號體系也是Hybrid設計中不可或缺的一環(huán)。

賬號體系涉及了接口權限控制、資源訪問控制,現在有一種方案是,前端代碼不做接口鑒權,賬號一塊的工作全部放到native端。

native代理請求

在H5想要做某一塊老的App業(yè)務,這個APP80%以上的業(yè)務都是Native做的,這類APP在接口方面就沒有考慮過H5的感受,會要求很多信息如:

① 設備號

② 地理信息

③ 網絡情況

④ 系統(tǒng)版本

有很多H5拿不到或者不容易拿到的公共信息,因為H5做的往往是一些比較小的業(yè)務,像什么個人主頁之類的不重要的業(yè)務,Server端可能不愿意提供額外的接口適配,而使用額外的接口還有可能打破他們統(tǒng)一的某些規(guī)則;加之native對接口有自己的一套公共處理邏輯,所以便出了Native代理H5發(fā)請求的方案,公共參數會由Native自動帶上。

復制代碼
 1 //暫時只關注hybrid調試,后續(xù)得關注三端匹配  2 _.requestHybrid({  3 tagname: 'apppost',  4  param: {  5 url: this.url,  6  param: params  7  },  8  9 callback: function (data) { 10  scope.baseDataValidate(data, onComplete, onError); 11  } 12 });
復制代碼

這種方案有一些好處,接口統(tǒng)一,前端也不需要關注接口權限驗證,但是這個會帶給前端噩夢!

前端相對于native一個很大的優(yōu)點,就是調試靈活,這種代理請求的方式,會限制請求只能在APP容器中生效,對前端調試造成了很大的痛苦

從真實的生產效果來說,也是很影響效率的,容易導致后續(xù)前端再也不愿意做那個APP的業(yè)務了,所以使用要慎重......

注入cookie

前端比較通用的權限標志還是用cookie做的,所以Hybrid比較成熟的方案仍舊是注入cookie,這里的一個前提就是native&H5有一套統(tǒng)一的賬號體系(統(tǒng)一的權限校驗系統(tǒng))。

因為H5使用的webview可以有獨立的登錄態(tài),如果不加限制太過混亂難以維護,比如:

我們在qq瀏覽器中打開攜程的網站,攜程站內第三方登錄可以喚起qq,然后登錄成功;完了qq瀏覽器本來也有一個登錄態(tài),發(fā)現卻沒有登錄,點擊一鍵登錄的時候再次喚起了qq登錄。

當然,qq作為一個瀏覽器容器,不應該關注業(yè)務的登錄,他這樣做是沒問題的,但是我們自己的一個H5子應用如果登錄了的話,便希望將這個登錄態(tài)同步到native,這里如果native去監(jiān)控cookie的變化就太復雜了,通用的方案是:

Hybrid APP中,所有的登錄走Native提供的登錄框

每次打開webview native便將當前登錄信息寫入cookie中,由此前端就具有登錄態(tài)了,登錄框的喚起在接口處統(tǒng)一處理:

復制代碼
 1 /*  2 無論成功與否皆會關閉登錄框  3 參數包括:  4 success 登錄成功的回調  5 error 登錄失敗的回調  6 url 如果沒有設置success,或者success執(zhí)行后沒有返回true,則默認跳往此url  7 */  8 HybridUI.Login = function (opts) {  9 }; 10 //=> 11 requestHybrid({ 12 tagname: 'login', 13  param: { 14 success: function () { }, 15 error: function () { }, 16 url: '...' 17  } 18 }); 19 //與登錄接口一致,參數一致 20 HybridUI.logout = function () { 21 };
復制代碼

賬號切換&注銷

賬戶注銷本沒有什么注意點,但是因為H5 push了一個個webview頁面,這個重新登錄后這些頁面怎么處理是個問題。

我們這邊設計的是一旦重新登錄或者注銷賬戶,所有的webview都會被pop掉,然后再新開一個頁面,就不會存在一些頁面展示怪異的問題了。

公共業(yè)務的設計-體系化

在Hybrid架構中(其實就算在傳統(tǒng)的業(yè)務中也是),會存在很多公共業(yè)務,這部分公共業(yè)務很多是H5做的(比如注冊、地址維護、反饋等,登錄是native化了的公共業(yè)務),我們一個Hybrid架構要真正的效率高,就得把各種公共業(yè)務做好了,不然單是H5做業(yè)務,效率未必會真的比Native高多少。

底層框架完善并且統(tǒng)一后,便可以以規(guī)范的力量限制各業(yè)務開發(fā),在統(tǒng)一的框架下開發(fā)出來的公共業(yè)務會大大的提升整體工作效率,這里以注冊為例,一個公共頁面一般來說得設計成這個樣子:

公共業(yè)務代碼,應該可以讓人在URL參數上對頁面進行一定定制化,這里URL參數一般要獨特一些,一面被覆蓋,這個設計適用于native頁面

URL中會包含以下參數:

① _hashead 是否有head,默認true

② _hasback 是否包含回退按鈕,默認true

③ _backtxt 回退按鈕的文案,默認沒有,這個時候顯示為回退圖標

④ _title 標題

⑤ _btntxt 按鈕的文案

⑥ _backurl 回退按鈕點擊時候的跳轉,默認為空則執(zhí)行history.back

⑦ _successurl 點擊按鈕回調成功時候的跳轉,必須

只要公共頁面設計為這個樣子,就能滿足多數業(yè)務了,在底層做一些適配,可以很輕易的一套代碼同時用于native與H5,這里再舉個例子:

如果我們要點擊成功后去到一個native頁面,如果按照我們之前的設計,我們每個Native頁面皆已經URL化了的話,我們完全可以以這種方向跳轉:

復制代碼
1 requestHybrid({ 2 tagname: 'forward', 3  param: { 4 topage: 'nativeUrl', 5 type: 'native' 6  } 7 });
復制代碼

這個命令會生成一個這樣的url的鏈接:

_successurl == hybrid://forward?param=%7B%22topage%22%3A%22nativeUrl%22%2C%22type%22%3A%22native%22%7D

完了,在點擊回調時要執(zhí)行一個H5的URL跳轉:

window.location = _successurl

而根據我們之前的hybrid規(guī)范約定,這種請求會被native攔截,于是就跳到了我們想要的native頁面,整個這一套東西就是我們所謂的體系化:

離線更新

根據之前的約定,Native中如果存在靜態(tài)資源,也是按頻道劃分的:

復制代碼
webapp //根目錄 ├─flight
├─hotel //酒店頻道 │  │  index.html //業(yè)務入口html資源,如果不是單頁應用會有多個入口 │  │  main.js //業(yè)務所有js資源打包 │  │
│  └─static //靜態(tài)樣式資源 │      ├─css 
│      ├─hybrid //存儲業(yè)務定制化類Native Header圖標 │      └─images
├─libs
│      libs.js //框架所有js資源打包 │
└─static //框架靜態(tài)資源樣式文件  ├─css
    └─images
復制代碼

我們這里制定一個規(guī)則,native會過濾某一個規(guī)則的請求,檢查本地是否有該文件,如果本地有那么就直接讀取本地,比如說,我們會將這個類型的請求映射到本地:

http://domain.com/webapp/flight/static/hybrid/icon-search.png
//===>>
file ===> flight/static/hybrid/icon-search.png

這樣在瀏覽器中便繼續(xù)讀取線上文件,在native中,如果有本地資源,便讀取本地資源:

但是我們在真實使用場景中卻遇到了一些麻煩。

增量的粒度

其實,我們最開始做增量設計的時候就考慮了很多問題,但是真實業(yè)務的時候往往因為時間的壓迫,做出來的東西就會很簡陋,這個只能慢慢迭代,而我們所有的緩存都會考慮兩個問題:

① 如何存儲&讀取緩存

② 如何更新緩存

瀏覽器的緩存讀取更新是比較單純的:

瀏覽器只需要自己能讀到最新的緩存即可

而APP的話,會存在最新發(fā)布的APP希望讀到離線包,而老APP不希望讀到增量包的情況(老的APP下載下來增量包壓根不支持),更加復雜的情況是想對某個版本做定向修復,那么就需要定向發(fā)增量包了,這讓情況變得復雜,而復雜即錯誤,我們往往可以以簡單的約定,解決復雜的場景。

思考以下場景:

我們的APP要發(fā)一個新的版本了,我們把最初一版的靜態(tài)資源給打了進去,完了審核中的時候,我們老版本APP突然有一個臨時需求要上線,我知道這聽起來很有一些扯淡,但這種扯淡的事情卻真實的發(fā)生了,這個時候我們如果打了增量包的話,那么最新的APP在審核期間也會拉到這次代碼,但也許這不是我們所期望的,于是有了以下與native的約定:

Native請求增量更新的時候帶上版本號,并且強迫約定iOS與Android的大版本號一致,比如iOS為2.1.0Android這個版本修復BUG可以是2.1.1但不能是2.2.0

然后在服務器端配置一個較為復雜的版本映射表:

復制代碼
## 附錄一 // 每個app所需的項目配置 const APP_CONFIG = [ 'surgery' => [ // 包名 'channel' => 'd2d', // 主項目頻道名 'dependencies' => ['blade', 'static', 'user'], // 依賴的頻道 'version' => [ // 各個版本對應的增量包范圍,取范圍內版本號最大的增量包 '2.0.x' => ['gte' => '1.0.0', 'lt' => '1.1.0'], '2.2.x' => ['gte' => '1.1.0', 'lt' => '1.2.0']
        ], 'version_i' => [ // ios需特殊配置的某版本  ], 'version_a' => [ // Android需特殊配置的某版本  ]
    ]
];
復制代碼

這里解決了APP版本的讀取限制,完了我們便需要關心增量的到達率與更新率,我們也會擔心我們的APP讀到錯誤的文件。

更新率

我們有時候想要的是一旦增量包發(fā)布,用戶拿著手機就馬上能看到最新的內容了,而這樣需要app調用增量包的頻率增高,所以我們是設置每30分鐘檢查一次更新。

正確讀取

這里可能有點杞人憂天,因為Native程序不是自己手把手開發(fā)的,總是擔心APP在正在拉取增量包時,或者正在解壓時,讀取了靜態(tài)文件,這樣會不會讀取錯誤呢,后面想了想,便繼續(xù)采用了之前的md5打包的方式,將落地的html中需要的文件打包為md5引用,如果落地頁下載下來后,讀不到本地文件就自己會去拉取線上資源咯。 

調試

一個Hybrid項目,要最大限度的符合前端的開發(fā)習慣,并且要提供可調試方案

我們之前說過直接將所有請求用native發(fā)出有一個最大的問題就是調試不方便,而正確的hybrid的開發(fā)應該是有70%以上的時間,純業(yè)務開發(fā)者不需要關心native聯調,當所有業(yè)務開發(fā)結束后再內嵌簡單調一下即可。

因為調試時候需要讀取測試環(huán)境資源,需要server端qa接口有個全局開關,關閉所有的增量讀取

關于代理調試的方法已經很多人介紹過了,我這里不再多說,說一些native中的調試方案吧,其實很多人都知道。

iOS

首先,你需要擁有一臺Mac機,然后打開safari;在偏好設置中將開發(fā)模式打開:

然后打開模擬器,即可開始調試咯:

Android

Android需要能FQ的chrome,然后輸入chrome://inspect/#devices即可,前提是native同事為你打開調試模式,當然Android也可以使用模擬器啦,但是Android的真機表現過于不一樣,還是建議使用真機測試。

一些坑點

不要命就用swift

蘋果官方出了swift,于是我們iOS團隊好事者嘗試了感覺不錯,便迅速在團隊內部推廣了起來,而我們OC本身的體量本來就有10多萬行代碼量,我們都知道一個道理:

重構一時爽,項目火葬場

而重構過程中肯定又會遇到一些歷史問題,或者一些第三方庫,代碼總會有一點尿不盡一點冗余,而不知道swift是官方有問題還是怎么回事,每次稍微多一些改動就需要編譯一個多小時?。。?!你沒看錯,是要編譯一個多小時。

一次,我的小伙伴在打游戲,被我揪著說了兩句,他說他在編譯,我尼瑪很不屑的罵了他,后面開始調iOS時,編譯了2小時!??!從那以后看見他打游戲我一點脾氣都沒有了?。?!

這種編譯的感覺,就像吃壞了肚子,在廁所蹲了半天卻什么也沒拉出來一樣!??!所以,不要命就全部換成swift吧。

如果有一定歷史包袱的業(yè)務,或者新業(yè)務,最好不要全面使用新技術,不成熟的技術,如果有什么不可逆的坑,那么會連一點退路都沒有了。

iOS靜態(tài)資源緩存

Android有一個全局開關,控制靜態(tài)資源部讀取緩存,但是iOS中研究了好久,都沒有找到這個開關,而他讀取緩存又特別厲害,所以所有的請求資源在有增量包的情況下,最好加上時間戳或者md5

Android webview兼容

Android webview的表現不佳,閃屏等問題比較多,遇到的幾個問題有:

① 使用hybrid命令(比如跳轉),如果點擊快了的話,Android因為響應慢要開兩個新頁面,需要對連續(xù)點擊做凍結

② 4.4以下低版本不能捕獲js回調,意思是Android拿不到js的返回值,一些特殊的功能就做不了,比如back容錯

③ ......

一些小特性

為了讓H5的表現更加像native我們會約定一些小的特性,這種特性不適合通用架構,但是有了會更有亮點。

回退更新

我們在hybrid中的跳轉,事實上每次都是新開一個webview,當A->B的時候,事實上A只是被隱藏了,當B點擊返回的時候,便直接將A展示了出來,而A不會做任何更新,對前端來說是無感知的。

事實上,這個是一種優(yōu)化,為了解決這種問題我們做了一個下拉刷新的特性:

復制代碼
 1 _.requestHybrid({  2 tagname: 'headerrefresh',  3  param: {  4 //下拉時候展示的文案  5 title: '123'  6  },  7 //下拉后執(zhí)行的回調,強暴點就全部刷新  8 callback: function(data) {  9  location.reload(); 10  } 11 });
復制代碼

但,這個總沒有自動刷新來的舒服,于是我們在頁面第一次加載的時候約定了這些事件:

復制代碼
 1 // 注冊頁面加載事件  2  _.requestHybrid({  3 tagname: 'onwebviewshow',  4 callback: function () {  5  6  }  7  });  8 // 注冊頁面影藏事件  9 _.requestHybrid({ 10 tagname: 'onwebviewhide', 11 callback: function () { 12 scope.loopFlag = false; 13  clearTimeout(scope.t); 14  } 15 });
復制代碼

在webview展示的時候觸發(fā),和在webview隱藏的時候觸發(fā),這樣用戶便可以做自動數據刷新了,但是局部刷新要做到什么程度就要看開發(fā)的時間安排了,技術好時間多自然體驗好。

header-搜索

根據我們之前的約定,header是比較中規(guī)中矩的,但是由于產品和視覺強迫,我們實現了一個不一樣的header,最開始雖然不太樂意,做完了后感覺還行......

這塊工作量主要是native的,我們只需要約定即可:

復制代碼
 1 this.header.set({  2 view: this,  3 //左邊按鈕  4  left: [],  5 //右邊按鈕  6  right: [{  7 tagname: 'cancel',  8 value: '取消',  9 callback: function () { 10 this.back(); 11  } 12  }], 13 //searchbox定制 14  title: { 15 //特殊tagname 16 tagname: 'searchbox', 17 //標題,該數據為默認文本框文字 18 title: '取消', 19 //沒有文字時候的占位提示 20 placeholder: '搜索醫(yī)院、科室、醫(yī)生和病癥', 21 //是否默認進入頁面獲取焦點 22 focus: true, 23 24 //文本框相關具有的回調事件 25 //data為一個json串 26 //editingdidbegin 為點擊或者文本框獲取焦點時候觸發(fā)的事件 27 //editingdidend 為文本框失去焦點觸發(fā)的事件 28 //editingchanged 為文本框數據改變時候觸發(fā)的事件 29 type: '', 30 data: '' //真實數據 31  }, 32 callback: function(data) { 33 var _data = JSON.parse(data); 34 if (_data.type == 'editingdidend' && this.keyword != $.trim(_data.data)) { 35 this.keyword = $.trim(_data.data); 36 this.reloadList(); 37  } 38 39  } 40 });
復制代碼

結語

希望此文能對準備接觸Hybrid技術的朋友提供一些幫助,關于Hybrid的系列這里是最后一篇實戰(zhàn)類文章介紹,這里是demo期間的一些效果圖,后續(xù)git庫的代碼會再做整理:

 

落地項目

真實落地的業(yè)務為醫(yī)聯通,有興趣的朋友試試: