根據之前的介紹,大家對前端與Native的互動應該有一些簡單的認識了,很多朋友就會覺得這個互動很簡單嘛,其實並不難嘛,事實上單從Native與前端的互動來說就那點東西,真心沒有太多可說的,但要真正做一個完整的Hybrid專案卻不容易,要考慮的東西就比較多了,單從這個互動協議就有:
① URL Schema
② JavaScriptCore
兩種,到底選擇哪種方式,每種方式有什麼優勢,都是我們需要深度挖掘的,而除此之外,一個Hybrid專案還應該具有以下特性:
① 擴充套件性好——依靠好的約定
② 開發效率高——依賴公共業務
③ 互動體驗好——需要解決各種相容問題
我們在實際工作中如何落地一個Hybrid專案,如何推動一個專案的進行,這是本次我們要討論的,也希望對各位有用。
文中是我個人的一些開發經驗,希望對各位有用,也希望各位多多支援討論,指出文中不足以及提出您的一些建議。
設計類部落格
http://www.cnblogs.com/yexiaochai/p/4921635.html
http://www.cnblogs.com/yexiaochai/p/5524783.html
http://www.cnblogs.com/nildog/p/5536081.html#3440931
Android部落格
https://home.cnblogs.com/u/vanezkw
程式碼地址:https://github.com/yexiaochai/Hybrid
因為IOS不能掃碼下載了,大家自己下載下來用模擬器看吧,下面開始今天的內容。
總體概述在第一章,有興趣大家去看
細節設計在第二章,有興趣大家去看
本章主要為打補丁
邊界問題
在我們使用Hybrid技術前要注意一個邊界問題,什麼專案適合Hybrid什麼專案不適合,這個要搞清楚,適合Hybrid的專案為:
① 有60%以上的業務為H5
② 對更新(開發效率)有一定要求的APP
不適合使用Hybrid技術的專案有以下特點:
① 只有20%不到的業務使用H5做
② 互動效果要求較高(動畫多)
任何技術都有適用的場景,千萬不要妄想推翻已有APP的業務用H5去替代,最後會證明那是自討苦吃,當然如果僅僅想在APP裡面嵌入新的實驗性業務,這個是沒問題的。
互動約定
根據之前的學習,我們知道與Native互動有兩種互動:
① URL Schema
② JavaScriptCore
而兩種方式在使用上各有利弊,首先來說URL Schema是比較穩定而成熟的,如果使用上文中提到的“ajax”互動方式,會比較靈活;而從設計的角度來說JavaScriptCore似乎更加合理,但是我們在實際使用中卻發現,注入的時機得不到保障。
iOS同事在實體JavaScriptCore注入時,我們的原意是在webview載入前就注入所有的Native能力,而實際情況是頁面js已經執行完了才被注入,這裡會導致Hybrid互動失效,如果你看到某個Hybrid平臺,突然header顯示不正確了,就可能是這個問題導致,所以JavaScriptCore就被我們棄用了。
1 2 3 |
JavaScriptCore可能導致的問題: ① 注入時機不唯一(也許是BUG) ② 重新整理頁面的時候,JavaScriptCore的注入在不同機型表現不一致,有些就根本不注入了,所以全部hybrid互動失效 |
如果非要使用JavaScriptCore,為了解決這一問題,我們做了一個相容,用URL Schema的方式,在頁面邏輯載入之初執行一個命令,將native的一些方式重新載入,比如:
1 2 3 |
_.requestHybrid({ tagname: 'injection' }); |
這個能解決一些問題,但是有些初始化就馬上要用到的方法可能就無力了,比如:
① 想要獲取native給予的地理資訊
② 想要獲取native給予的使用者資訊(直接以變數的方式獲取)
作為生產來講,我們還是求穩,所以最終選擇了URL Schema。
明白了基本的邊界問題,選取了底層的互動方式,就可以開始進行初步的Hybrid設計了,但是這離一個可用於生產,可離落地的Hybrid方案還比較遠。
賬號體系
一般來說,一個公司的賬號體系健壯靈活程度會很大程度反映出這個研發團隊的整體實力:
① 統一的鑑權認證
② 簡訊服務圖形驗證碼的處理
③ 子系統的許可權設計、公共的使用者資訊匯出
④ 第三方接入方案
⑤ 接入文件輸出
⑥ ……
這個技術方案,有沒有是一回事(說明沒思維),有幾套是一回事(說明比較亂,技術不統一),對外的一套做到了什麼程度又是一回事,當然這個不是我們討論的重點,而賬號體系也是Hybrid設計中不可或缺的一環。
賬號體系涉及了介面許可權控制、資源訪問控制,現在有一種方案是,前端程式碼不做介面鑑權,賬號一塊的工作全部放到native端。
native代理請求
在H5想要做某一塊老的App業務,這個APP80%以上的業務都是Native做的,這類APP在介面方面就沒有考慮過H5的感受,會要求很多資訊如:
① 裝置號
② 地理資訊
③ 網路情況
④ 系統版本
有很多H5拿不到或者不容易拿到的公共資訊,因為H5做的往往是一些比較小的業務,像什麼個人主頁之類的不重要的業務,Server端可能不願意提供額外的介面適配,而使用額外的介面還有可能打破他們統一的某些規則;加之native對介面有自己的一套公共處理邏輯,所以便出了Native代理H5發請求的方案,公共引數會由Native自動帶上。
1 2 3 4 5 6 7 8 9 10 11 12 |
//暫時只關注hybrid除錯,後續得關注三端匹配 _.requestHybrid({ tagname: 'apppost', param: { url: this.url, param: params }, callback: function (data) { scope.baseDataValidate(data, onComplete, onError); } }); |
這種方案有一些好處,介面統一,前端也不需要關注介面許可權驗證,但是這個會帶給前端噩夢!
1 |
前端相對於native一個很大的優點,就是除錯靈活,這種代理請求的方式,會限制請求只能在APP容器中生效,對前端除錯造成了很大的痛苦 |
從真實的生產效果來說,也是很影響效率的,容易導致後續前端再也不願意做那個APP的業務了,所以使用要慎重……
注入cookie
前端比較通用的許可權標誌還是用cookie做的,所以Hybrid比較成熟的方案仍舊是注入cookie,這裡的一個前提就是native&H5有一套統一的賬號體系(統一的許可權校驗系統)。
因為H5使用的webview可以有獨立的登入態,如果不加限制太過混亂難以維護,比如:
我們在qq瀏覽器中開啟攜程的網站,攜程站內第三方登入可以喚起qq,然後登入成功;完了qq瀏覽器本來也有一個登入態,發現卻沒有登入,點選一鍵登入的時候再次喚起了qq登入。
當然,qq作為一個瀏覽器容器,不應該關注業務的登入,他這樣做是沒問題的,但是我們自己的一個H5子應用如果登入了的話,便希望將這個登入態同步到native,這裡如果native去監控cookie的變化就太複雜了,通用的方案是:
1 |
Hybrid APP中,所有的登入走Native提供的登入框 |
每次開啟webview native便將當前登入資訊寫入cookie中,由此前端就具有登入態了,登入框的喚起在介面處統一處理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* 無論成功與否皆會關閉登入框 引數包括: success 登入成功的回撥 error 登入失敗的回撥 url 如果沒有設定success,或者success執行後沒有返回true,則預設跳往此url */ HybridUI.Login = function (opts) { }; //=> requestHybrid({ tagname: 'login', param: { success: function () { }, error: function () { }, url: '...' } }); //與登入介面一致,引數一致 HybridUI.logout = function () { }; |
賬號切換&登出
賬戶登出本沒有什麼注意點,但是因為H5 push了一個個webview頁面,這個重新登入後這些頁面怎麼處理是個問題。
我們這邊設計的是一旦重新登入或者登出賬戶,所有的webview都會被pop掉,然後再新開一個頁面,就不會存在一些頁面展示怪異的問題了。
公共業務的設計-體系化
在Hybrid架構中(其實就算在傳統的業務中也是),會存在很多公共業務,這部分公共業務很多是H5做的(比如註冊、地址維護、反饋等,登入是native化了的公共業務),我們一個Hybrid架構要真正的效率高,就得把各種公共業務做好了,不然單是H5做業務,效率未必會真的比Native高多少。
底層框架完善並且統一後,便可以以規範的力量限制各業務開發,在統一的框架下開發出來的公共業務會大大的提升整體工作效率,這裡以註冊為例,一個公共頁面一般來說得設計成這個樣子:
1 |
公共業務程式碼,應該可以讓人在URL引數上對頁面進行一定定製化,這裡URL引數一般要獨特一些,一面被覆蓋,這個設計適用於native頁面 |
URL中會包含以下引數:
① _hashead 是否有head,預設true
② _hasback 是否包含回退按鈕,預設true
③ _backtxt 回退按鈕的文案,預設沒有,這個時候顯示為回退圖示
④ _title 標題
⑤ _btntxt 按鈕的文案
⑥ _backurl 回退按鈕點選時候的跳轉,預設為空則執行history.back
⑦ _successurl 點選按鈕回撥成功時候的跳轉,必須
只要公共頁面設計為這個樣子,就能滿足多數業務了,在底層做一些適配,可以很輕易的一套程式碼同時用於native與H5,這裡再舉個例子:
如果我們要點選成功後去到一個native頁面,如果按照我們之前的設計,我們每個Native頁面皆已經URL化了的話,我們完全可以以這種方向跳轉:
1 2 3 4 5 6 7 |
requestHybrid({ tagname: 'forward', param: { topage: 'nativeUrl', type: 'native' } }); |
這個命令會生成一個這樣的url的連結:
_successurl == hybrid://forward?param=%7B%22topage%22%3A%22nativeUrl%22%2C%22type%22%3A%22native%22%7D
完了,在點選回撥時要執行一個H5的URL跳轉:
1 |
window.location = _successurl |
而根據我們之前的hybrid規範約定,這種請求會被native攔截,於是就跳到了我們想要的native頁面,整個這一套東西就是我們所謂的體系化:
離線更新
根據之前的約定,Native中如果存在靜態資源,也是按頻道劃分的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
webapp //根目錄 ├─flight ├─hotel //酒店頻道 │ │ index.html //業務入口html資源,如果不是單頁應用會有多個入口 │ │ main.js //業務所有js資源打包 │ │ │ └─static //靜態樣式資源 │ ├─css │ ├─hybrid //儲存業務定製化類Native Header圖示 │ └─images ├─libs │ libs.js //框架所有js資源打包 │ └─static //框架靜態資源樣式檔案 ├─css └─images |
我們這裡制定一個規則,native會過濾某一個規則的請求,檢查本地是否有該檔案,如果本地有那麼就直接讀取本地,比如說,我們會將這個型別的請求對映到本地:
1 2 3 |
http://domain.com/webapp/flight/static/hybrid/icon-search.png //===>> file ===> flight/static/hybrid/icon-search.png |
這樣在瀏覽器中便繼續讀取線上檔案,在native中,如果有本地資源,便讀取本地資源:
但是我們在真實使用場景中卻遇到了一些麻煩。
增量的粒度
其實,我們最開始做增量設計的時候就考慮了很多問題,但是真實業務的時候往往因為時間的壓迫,做出來的東西就會很簡陋,這個只能慢慢迭代,而我們所有的快取都會考慮兩個問題:
① 如何儲存&讀取快取
② 如何更新快取
瀏覽器的快取讀取更新是比較單純的:
1 |
瀏覽器只需要自己能讀到最新的快取即可 |
而APP的話,會存在最新發布的APP希望讀到離線包,而老APP不希望讀到增量包的情況(老的APP下載下來增量包壓根不支援),更加複雜的情況是想對某個版本做定向修復,那麼就需要定向發增量包了,這讓情況變得複雜,而複雜即錯誤,我們往往可以以簡單的約定,解決複雜的場景。
思考以下場景:
我們的APP要發一個新的版本了,我們把最初一版的靜態資源給打了進去,完了稽核中的時候,我們老版本APP突然有一個臨時需求要上線,我知道這聽起來很有一些扯淡,但這種扯淡的事情卻真實的發生了,這個時候我們如果打了增量包的話,那麼最新的APP在稽核期間也會拉到這次程式碼,但也許這不是我們所期望的,於是有了以下與native的約定:
1 |
Native請求增量更新的時候帶上版本號,並且強迫約定iOS與Android的大版本號一致,比如iOS為2.1.0Android這個版本修復BUG可以是2.1.1但不能是2.2.0 |
然後在伺服器端配置一個較為複雜的版本對映表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
## 附錄一 // 每個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讀到錯誤的檔案。
更新率
我們有時候想要的是一旦增量包釋出,使用者拿著手機就馬上能看到最新的內容了,而這樣需要app呼叫增量包的頻率增高,所以我們是設定每30分鐘檢查一次更新。
正確讀取
這裡可能有點杞人憂天,因為Native程式不是自己手把手開發的,總是擔心APP在正在拉取增量包時,或者正在解壓時,讀取了靜態檔案,這樣會不會讀取錯誤呢,後面想了想,便繼續採用了之前的md5打包的方式,將落地的html中需要的檔案打包為md5引用,如果落地頁下載下來後,讀不到本地檔案就自己會去拉取線上資源咯。
除錯
1 |
一個Hybrid專案,要最大限度的符合前端的開發習慣,並且要提供可除錯方案 |
我們之前說過直接將所有請求用native發出有一個最大的問題就是除錯不方便,而正確的hybrid的開發應該是有70%以上的時間,純業務開發者不需要關心native聯調,當所有業務開發結束後再內嵌簡單調一下即可。
1 |
因為除錯時候需要讀取測試環境資源,需要server端qa介面有個全域性開關,關閉所有的增量讀取 |
關於代理除錯的方法已經很多人介紹過了,我這裡不再多說,說一些native中的除錯方案吧,其實很多人都知道。
iOS
首先,你需要擁有一臺Mac機,然後開啟safari;在偏好設定中將開發模式開啟:
然後開啟模擬器,即可開始除錯咯:
Android
Android需要能FQ的chrome,然後輸入chrome://inspect/#devices即可,前提是native同事為你開啟除錯模式,當然Android也可以使用模擬器啦,但是Android的真機表現過於不一樣,還是建議使用真機測試。
一些坑點
不要命就用swift
蘋果官方出了swift,於是我們iOS團隊好事者嘗試了感覺不錯,便迅速在團隊內部推廣了起來,而我們OC本身的體量本來就有10多萬行程式碼量,我們都知道一個道理:
1 |
重構一時爽,專案火葬場 |
而重構過程中肯定又會遇到一些歷史問題,或者一些第三方庫,程式碼總會有一點尿不盡一點冗餘,而不知道swift是官方有問題還是怎麼回事,每次稍微多一些改動就需要編譯一個多小時!!!!你沒看錯,是要編譯一個多小時。
一次,我的小夥伴在打遊戲,被我揪著說了兩句,他說他在編譯,我尼瑪很不屑的罵了他,後面開始調iOS時,編譯了2小時!!!從那以後看見他打遊戲我一點脾氣都沒有了!!!
這種編譯的感覺,就像吃壞了肚子,在廁所蹲了半天卻什麼也沒拉出來一樣!!!所以,不要命就全部換成swift吧。
1 |
如果有一定歷史包袱的業務,或者新業務,最好不要全面使用新技術,不成熟的技術,如果有什麼不可逆的坑,那麼會連一點退路都沒有了。 |
iOS靜態資源快取
Android有一個全域性開關,控制靜態資源部讀取快取,但是iOS中研究了好久,都沒有找到這個開關,而他讀取快取又特別厲害,所以所有的請求資源在有增量包的情況下,最好加上時間戳或者md5
Android webview相容
Android webview的表現不佳,閃屏等問題比較多,遇到的幾個問題有:
① 使用hybrid命令(比如跳轉),如果點選快了的話,Android因為響應慢要開兩個新頁面,需要對連續點選做凍結
② 4.4以下低版本不能捕獲js回撥,意思是Android拿不到js的返回值,一些特殊的功能就做不了,比如back容錯
③ ……
一些小特性
為了讓H5的表現更加像native我們會約定一些小的特性,這種特性不適合通用架構,但是有了會更有亮點。
回退更新
我們在hybrid中的跳轉,事實上每次都是新開一個webview,當A->B的時候,事實上A只是被隱藏了,當B點選返回的時候,便直接將A展示了出來,而A不會做任何更新,對前端來說是無感知的。
事實上,這個是一種優化,為了解決這種問題我們做了一個下拉重新整理的特性:
1 2 3 4 5 6 7 8 9 10 11 |
_.requestHybrid({ tagname: 'headerrefresh', param: { //下拉時候展示的文案 title: '123' }, //下拉後執行的回撥,強暴點就全部重新整理 callback: function(data) { location.reload(); } }); |
但,這個總沒有自動重新整理來的舒服,於是我們在頁面第一次載入的時候約定了這些事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 註冊頁面載入事件 _.requestHybrid({ tagname: 'onwebviewshow', callback: function () { } }); // 註冊頁面影藏事件 _.requestHybrid({ tagname: 'onwebviewhide', callback: function () { scope.loopFlag = false; clearTimeout(scope.t); } }); |
在webview展示的時候觸發,和在webview隱藏的時候觸發,這樣使用者便可以做自動資料重新整理了,但是區域性重新整理要做到什麼程度就要看開發的時間安排了,技術好時間多自然體驗好。
header-搜尋
根據我們之前的約定,header是比較中規中矩的,但是由於產品和視覺強迫,我們實現了一個不一樣的header,最開始雖然不太樂意,做完了後感覺還行……
這塊工作量主要是native的,我們只需要約定即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
this.header.set({ view: this, //左邊按鈕 left: [], //右邊按鈕 right: [{ tagname: 'cancel', value: '取消', callback: function () { this.back(); } }], //searchbox定製 title: { //特殊tagname tagname: 'searchbox', //標題,該資料為預設文字框文字 title: '取消', //沒有文字時候的佔位提示 placeholder: '搜尋醫院、科室、醫生和病症', //是否預設進入頁面獲取焦點 focus: true, //文字框相關具有的回撥事件 //data為一個json串 //editingdidbegin 為點選或者文字框獲取焦點時候觸發的事件 //editingdidend 為文字框失去焦點觸發的事件 //editingchanged 為文字框資料改變時候觸發的事件 type: '', data: '' //真實資料 }, callback: function(data) { var _data = JSON.parse(data); if (_data.type == 'editingdidend' && this.keyword != $.trim(_data.data)) { this.keyword = $.trim(_data.data); this.reloadList(); } } }); |
結語
希望此文能對準備接觸Hybrid技術的朋友提供一些幫助,關於Hybrid的系列這裡是最後一篇實戰類文章介紹,這裡是demo期間的一些效果圖,後續git庫的程式碼會再做整理:
落地專案
真實落地的業務為醫聯通,有興趣的朋友試試:
推動感悟
從專案調研到專案落地再到最近一些的優化,已經花了三個月時間了,要做好一件事是不容易的,而且我們這個還涉及到持續優化,和配套業務比如:
① passport
② 錢包業務
③ 反饋業務
…..
等同步製作,很多工作的意義,或者作用,是非技術同事看不到的,但是如果我們不堅持做下去,迫於業務壓力或者自我鬆懈放縱,那麼就什麼也沒有了,我們要推動一件事情,不可能一站出來就說,嘿,小樣,我們這個不錯,你拿去用吧,這樣人家會猜疑你的,我們一定是要先做一定demo讓人有一定初步印象,再強制或者偷偷再某一個生產業務試用,一方面將技術依賴弄進去,一方面要告訴其他同事,看看嘛,也沒有引起多大問題嘛,呵呵。
做事難,推動難,難在堅持,難在攜手共進,這裡面是需要信念的,在此尤其感謝團隊3個夥伴的無私付出(楊楊、文文、文文)。
後續,我們在持續推動hybrid建設的同時,會嘗試React Native,找尋更好的更適合自己的解決方案。