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

前言

淺談Hybrid技術的設計與實現javascript

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

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

接上文:淺談Hybrid技術的設計與實現(閱讀本文前,建議閱讀這個先)前端

上文說了不少關於Hybrid的概要設計,能夠算得上大而全,有說明有demo有代碼,對於想接觸Hybrid的朋友來講應該有必定幫助,可是對於進階的朋友可能就不太知足了,他們會想了解其中的每個細節,甚至是一些Native的實現,小釵這裏繼續拋磚引玉,但願接下來的內容對各位有必定幫助。java

進入今天的內容以前咱們首先談談兩個相關技術Ionic與React Native。ios

Ionic是一個基於Cordova的移動開發框架,他的一大優點是有一套配套的前端框架,由於是基於angularJS的,又提供了大量可用UI,可謂大而全,對開發效率頗有幫助;與咱們所說的Hybrid不一樣的是,咱們的Native容器是由公司或者我的定製開發,Ionic的Native殼是第三方公司作的平臺性產品,優劣不論,可是樓主是毫不會用太多第三方開源的東西的,舉個例子來講,你的項目中若是第三方UI組件越多,那麼性能和風險相對會越多,由於你不瞭解他。另外一方面angular對於H5來講,尺寸着實過大,最後以逼格來講,Ionic仍是多適用於外包了。git

與Ionic不一樣的是React Native,根據他寫出來的View徹底是Native的View,那麼這個逼格和體驗就高了,小釵在此次Hybrid項目結束後,應該會着力在這方面作研究,這裏沒實際經驗就很少說了。github

文中是我我的的一些開發經驗,但願對各位有用,也但願各位多多支持討論,指出文中不足以及提出您的一些建議web

設計類博客(還有最後一篇便完結)ajax

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

文中IOS代碼由我如今的同事Nil(http://www.cnblogs.com/nildog/p/5536081.html)提供,感謝Nil對項目的支持。

以前Android代碼由明月提供,後續明月也會持續支援咱們Android的實現,感謝明月。

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

由於IOS不能掃碼下載了,你們本身下載下來用模擬器看吧,下面開始今天的內容。

H5與Native通訊

Url Schema

根據以前的知識,H5與Native交互的橋樑爲Webview,而「聯繫」的方式是以url schema的方式作的,在用戶安裝app後,app能夠自定義url schema,而且把自定義的url註冊在調度中心, 例如

  • ctrip://wireless 打開攜程App
  • weixin:// 打開微信

事實上Native能捕捉webview發出的一切請求,因此就算這裏不是這種協議,Native也能捕捉,這個協議的意義在於能夠在瀏覽器中直接打開APP,相關文獻爲:

又到週末了,咱們一塊兒來研究【瀏覽器如何檢測是否安裝app】吧

這裏盜用一張以前的交互模型圖,確實懶得畫新的了:

 

咱們在H5獲取Native方法時通常是會構造一個這樣的請求,使用iframe發出(設置location會有屢次請求覆蓋的問題):

 1 requestHybrid({
 2   //建立一個新的webview對話框窗口
 3   tagname: 'hybridapi',
 4   //請求參數,會被Native使用
 5   param: {},
 6   //Native處理成功後回調前端的方法
 7   callback: function (data) {
 8   }
 9 });
10 //=====>
11 hybridschema://hybridapi?callback=hybrid_1446276509894&param=%7B%22data1%22%3A1%2C%22data2%22%3A2%7D

多數狀況下這種方式沒有問題,可是咱們在後續的開發中,爲了統一鑑權,將全部的請求所有代理到了Native發出,好比這樣:

 1 requestHybrid({
 2     tagname: 'post',
 3     param: {
 4         url: 'http://api.kuai.baidu.com/city/getstartcitys',
 5         param1: 'param1',
 6         param2: 'param2'
 7     },
 8     callback: function(data) {
 9     }
10 });

請注意,這裏但是POST請求,這裏首先考慮的是長度限制,畢竟這個是由iframe的src設置的,雖然各個瀏覽器不同,但一定會收到長度限制(2k),針對這個問題我諮詢了糯米以及攜程的Hybrid底層團隊,獲得了比較零星的回答:

① 移動端通常來講不會有這麼長的請求(這個在理)

② 咱們不支持IOS6了,如今用的JavaScriptCore

上面的答覆不太滿意,因而我嘗試在頁面上放一個全局變量(或者文本框)以解決參數過大的問題,而當我嘗試解決的時候,產品告訴我:咱們早不支持IOS6了

若是隻用支持chrome用戶,那麼堅定不支持IE的!抱着這一想法,小釵也就放棄IOS6了

若是不支持IOS6,那麼事情彷佛變得好辦多了。

JavaScriptCore

在ios7後,Apple新增了一個JavaScriptCore讓Native能夠與H5更好的交互(Android早就有了),咱們這裏主要討論js如何與Native通訊,這裏舉一個簡單的例子:

PS:樓主對ios不熟,這段代碼引至https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.html

① 首先定義一個js方法,這裏注意其中調用了一個沒有聲明的方法:

function printHello() {
    //未聲明方法
    print("Hello, World!");
}

而後,上述未聲明方法事實上是Native注入給window對象的:

 1 NSString *scriptPath = [[NSBundle mainBundle] pathForResource:@"hello" ofType:@"js"];
 2 NSString *scriptString = [NSString stringWithContentsOfFile:scriptPath encoding:NSUTF8StringEncoding error:nil];
 3 
 4 JSContext *context = [[JSContext alloc] init];
 5 [context evaluateScript:scriptString];
 6 
 7 self.context[@"print"] = ^(NSString *text) {  8     NSLog(@"%@", text");
 9 }; 10 
11 JSValue *function = self.context[@"printHello"];
12 [function callWithArguments:@[]];

這個樣子,JavaScript就能夠調用Native的方法了,這裏Native須要注意方法注入的時機,通常是一旦載入頁面便須要載入變量,這裏的交互模型是:

因而,咱們這裏只須要將原來底層的通訊一塊改下便可(Android自己就支持相似的實現,這裏暫時不予關注):

 1 //使用jsCore與native通訊
 2 window.requestNative && requestNative(JSON.stringify(params));
 3 return;
 4 //兼容ios6
 5 var ifr = $('<iframe style="display: none;" src="' + url + '"/>');
 6 $('body').append(ifr);
 7 setTimeout(function () {
 8     ifr.remove();
 9     ifr = null;
10 }, 1000)

優劣

URL Schema與JavaScriptCore的優劣很差說,仍是看具體使用場景吧,不考慮參數問題的話,真正有使用經驗的朋友可能會發現url schema方案可能更加適用,由於:

URL Schema方案是監控Webview請求,因此比較通用;而JavaScriptCore的注入是在Webview加載時候注入。
若是頁面刷新,這個注入也就丟了須要作特殊處理(糯米接入一個常見BUG就是糯米容器提供的方法不執行)

使用JavaScriptCore的話,頁面刷新會致使Hybrid項目癱瘓的問題,咱們IOS同事首先調整了注入方法的時間點,放到了webViewDidFinishLoad中,由於webViewDidFinishLoad的注入在頁面js聲明以後,因此若是一來就有Hybrid的交互便不會執行,好比:

1 //若是一開始便設置的話,將由於Native沒有注入而不執行
2 Hybrid.ui.header.set({
3     title: '設置右邊按鈕',
4 });

因此我與Native約定在webViewDidFinishLoad後執行一個我定義的方法,咱們將頁面初始化邏輯放到這個事件裏面,好比:

1 Hybrid.ready = function() {
2     hybridInit();
3 }

對比這個方法與以前jQuery的dom ready有點相似,咱們可能會擔憂這樣會影響頁面的渲染速度,這裏特別作了一個測試,這段代碼對真實邏輯執行確實有必定影響,首次啓動在30-90ms之間,第二次沒什麼影響了,這裏也造成了一個一個優化點,只將那種頁面一加載結束就須要執行的邏輯放入其中,影響主頁面的邏輯可優先執行,若是以爲麻煩便直接將頁面的載入放到這個方法中便可。

選擇建議

根據咱們的使用過程,發現JavaScriptCore仍是很差用,由於對Native的不熟悉,在js方法注入的時間點一塊咱們踩了一些坑,咱們想在webViewDidFinishLoad中注入方法,可是發現有必定概率是頁面js已經執行完了才注入,致使Hybrid交互失效。

並且咱們對Native一塊聲明js方法的生命週期與垃圾回收一塊也不熟悉,總擔憂埋下什麼坑,加之以前30-90ms的延遲,咱們最終是實現了兩套方案:

通常狀況下仍舊使用URL Schema,若是有不知足的場景,咱們會使用JavaScriptCore,由於底層架構搭建不能耗費太多時間,因此對JavaScriptCore的研究便暫時到此,後續有時間須要對他作深刻研究。

Hybrid版本

APP會有版本號概念,每一個版本會加一些新的特性或者會改一些BUG,通常的版本號是1.0.0,若是改了BUG打了補丁是1.0.1,有新特性就是1.1.0,若是有大改變的話就2.0.0咯,咱們在實際業務代碼中可能會有用到版本號的地方,因此Native須要將當前版本號告訴咱們,通常是採用Native篡改navigator.userAgent寫入特殊標誌實現,咱們這裏是寫入了這種標識:

xxx hybrid_1.0.0 xxx

而後咱們會在全局釋放一個工具類方法獲取當前版本號:

 1 var getHybridInfo = function () {
 2     var platform_version = {};
 3     var na = navigator.userAgent;
 4     na = na.toLowerCase();
 5     var info = na.match(/hybrid_\d\.\d\.\d/);
 6     if (info && info[0]) {
 7         info = info[0].split('_');
 8         if (info && info.length == 2) {
 9             platform_version.platform = info[0];
10             platform_version.version = info[1];
11         }
12     }
13     return platform_version;
14 };

因而,咱們在業務開發中便能知道當前是否是處於Native容器中,和獲取版本號。

根據以前的共識,咱們的代碼只要運行在Native容器中就應該表現的像Hybrid,在瀏覽器中就應該表現的像H5

上面這句話可能不少朋友以爲有點奇怪,這裏的界限在於有些方法H5提供了Native也提供了,究竟該用哪一個的問題,好比獲取當前位置信息,若是在Native容器中天然走Native獲取,若是在瀏覽器中那就走H5接口。

交互格式約定

作一件事情重中之重的就是基礎約定,咱們這裏作Hybrid架構首先就要作好交互格式約定,這種格式約定的要靈活一點,這個將會在後續擴展中提現他的優點,咱們這裏依舊採用相似Ajax的交互規則:

請求格式

1 requestHybrid({
2   //建立一個新的webview對話框窗口
3   tagname: 'hybridapi',
4   //請求參數,會被Native使用
5   param: {},
6   //Native處理成功後回調前端的方法
7   callback: function (data) {
8   }
9 });

tagname是標誌此次請求的惟一標識,在接口比較多的狀況有可能會有命名空間,好比:tagname: 'ns/api'。

回調格式

回調的方式都是Native調用H5的js方法,前端須要告訴Native去哪一個對象拿回調方法,另外前端須要與Native約定返回時所帶的參數,咱們是這樣設計的:

{
  data: {},
  errno: 0,
  msg: "success"
}

其中每一個錯誤碼須要詳細的約定,好比:

{
  data: {},
  errno: 1,
  msg: "APP版本太低,請升級APP版本"
}

可是真實業務調用的時候卻不須要特別去處理響應數據,由於前端應該有統一的地方處理,到具體業務回調時應該只須要使用data數據便可。

調用方式的困惑

通常來講,H5與Native通訊都只會使用一個方法,咱們以前是H5建立url schema,後面有有了新的方案,是Native釋放一個requestNative給H5調用,這裏就產生了一個以前沒有的問題:

以前Native是沒有能力將具體API方法注入給H5,因此咱們使用惟一的方法傳遞tagname給Native,Native底層會使用相似反射的方式執行他的邏輯,這個tagname能夠理解爲方法名,而如今Native是有能力爲前端注入全部須要的方法了,好比:

意思是以前要根據url schema而後native捕捉請求後,獲取tagname字符串,再映射到具體NativeAPI,而如今Native已經有能力將這些Native API創建一個映射函數,注入給H5,因此H5能夠直接調用這些方法了,實際的例子是:

 1 //全部請求交互收口到一個方法,方法內部再作具體處理
 2 requestHybrid({
 3     tagname: 'getAdress',
 4     param: {
 5         param: 'param'
 6     },
 7     callback: function(data){}
 8 });
 9 
10 //每一個請求交互獨立調用Native注入接口
11 hybrid.getAdress({
12     param: {
13         param: 'param'
14     },
15     callback: function(data){}
16 });

這裏能夠各位須要產生一個思考,方案一與方案二到底選哪一個?這個時候就要多考慮框架的擴展性了,一旦有機會「收口」的都要考慮 「收口」,咱們對某一類方法應該有統一的收口的地方,以便處理咱們一些公共的邏輯,好比:

① 前端要對每一個接口調用的次數打點

② 前端要對參數作統一處理

③ 咱們忽然要在底層改變與APP橋接的方式,不能走JavaScriptCore了(咱們就實際遇到了這個問題)

④ 前端要爲Native返回的錯誤碼作統一封裝

......

一個比較好的交互事實上是這樣的,請求的時候要經過一個地方的處理,回調的時候也須要經過一個地方的處理,而這個地方即是咱們能統一把關與控制的地方了,正如咱們對ajax的收口,對settimeout的收口是一個道理:

跳轉

不管什麼系統,一個最重要的功能就是跳轉,跳轉設計的好壞很大程度決定你的框架好很差,好的跳轉設計能夠省下業務不少功夫,對迭代擴展也頗有幫助,對於Hybrid來講,跳轉可能會有:

① Native跳H5

② H5跳Native

③ H5跳H5(這裏還要份內嵌的場景)

④ H5新開Webview打開H5

......

通常來講,Native跳H5事實上是用不着咱們關注的,可是有一類Native跳轉咱們還不得不關注。

入口類跳轉

所謂入口類跳轉有如下特色:

① 一個入口每每會跳到一個獨立的頻道

② 每一個獨立的入口的實現首頁關注不了

③ 頻道多是Native的,也多是Hybrid的

幾個常見的狀況是:

 

好比糯米的美食或者攜程的酒店,美食是Hybrid的,酒店是Native的,而跳轉實現是作到Native上的,須要考慮到他的靈活性,也就是我一次點擊想去哪就去哪,這個天然須要數據庫的支持。

事實上在這類「入口類」跳轉模塊,每一個模塊點擊往哪裏跳轉可能server端會給他一個相似這樣的數據結構:

 1 //跳Native
 2 {
 3     topage: 'hotel/index',
 4     type: 'native'
 5 }
 6 //跳轉H5
 7 {
 8     topage: 'https://mdianying.baidu.com',
 9     type: 'h5'
10 }

固然,上述只是可能的數據結構,根據以前咱們的實現,更有多是這個樣子,直接只是一個URL:

1 requestHybrid({
2     tagname: 'forward',
3     param: {
4         topage: 'train/index.html',
5         type: 'h5'
6     }
7 });
8 //=>
9 hybrid://forward?param=%7B%22topage%22%3A%22hotel%2Findex.html%22%2C%22type%22%3A%22h5%22%7D

以這個作法來講,不管是怎麼跳轉,仍然能夠統一將實現封裝到forward的實現中。

若是你使用的是JavaScriptCore,URL Schema依舊要保留以處理這類跳轉或者外部瀏覽器打開APP的需求,有時候當一種方案坑的時候才能體現另外一種的難得。

動畫約定

Native體驗好,其中一個緣由就是有過場動畫,咱們這裏約定了四種基本的動畫效果:

//默認動畫push 左進
requestHybrid({
    tagname: 'forward',
    param: {
        topage: 'index2',
        type: 'native'
    }
});
//右出
requestHybrid({
    tagname: 'forward',
    param: {
        topage: 'index2',
        type: 'native',
        animate: 'pop'
    }
});
//從下往上動畫,這種關閉的時候會自動從上向下滑出
requestHybrid({
    tagname: 'forward',
    param: {
        topage: 'index2',
        type: 'native',
        animate: 'present'
    }
});

若是沒有動畫animate參數便設置爲none便可。

back

由於要保證H5與Native的特性一致,Native的頁面路徑事實上也是與瀏覽器一致的,因此咱們只須要保證Native中的back與瀏覽器中同樣,意思是什麼都不作......

requestHybrid({
    tagname: 'back'
});

這裏back在webview中會檢查history的記錄,若是大於1則後退,不然會退回上一步操做。咱們能夠看出,back的功能是很單一的,每每不能知足咱們的需求,因此經常使用forward+pop動畫當作back使用,而這一作法將引發使人頭疼的history錯亂問題!!!

forward

forward是很是重要的一個API,在Hybrid中狀況又比較複雜,因此這塊要花點力氣多思考,設計的好很差直接影響業務開發的接受情感。

我以前作框架時會禁止業務開發使用label標籤與a標籤,這個舉動也受到了一些質疑(每每是語義化)
其實並非label標籤和a標籤很差,而是解決移動端300ms延遲可能會對label標籤作特殊處理,容易引發一些莫名其妙的BUG;
而a標籤更是破壞單頁應用路由的最佳選手,不少同事爲a標籤添加點擊事件(沒有設置href)又沒有阻止默認事件而致使意想不到的BUG
像這種時候,你與其給人一個個解釋要如何作特殊處理,倒不如直接禁止他們使用來得快......

H5跳Native

H5跳Native比較簡單,只須要與Native同事約定topage的頁面便可

1 requestHybrid({
2     tagname: 'forward',
3     param: {
4         topage: 'index2',
5         type: 'native'
6     }
7 });

若是要帶參數的話,便直接寫到topage後面的參數上:

topage: 'index2?a=1&b=2',

這個寫法顯然是有點怪的,由於咱們以前跳轉是這樣寫的:

this.forward('index2', {
  a: 1, b: 2
});

爲了保證業務代碼一致,咱們只須要在前端底層封裝forward方法便可,這個將生成這種url,根據咱們url schema的約定,這個連接就會進入到Native對應頁面:

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

H5新開Webview跳H5

原本H5跳H5走的是瀏覽器內部體系,但爲加強體驗會新開一個Webview作動畫,儘量的模擬Native交互,這個代碼是這樣的:

requestHybrid({
    tagname: 'forward',
    param: {
        //flight/detail.html?id=111
        //hotel/index.html
        //http:www.baidu.com
        topage: 'flight/index.html',
        type: 'h5'
    }
});

若是一個團隊前端成體系的話,通常每一個頻道的代碼是有規則的,通常是頻道名/頁面名,事實上每一個topage都應該是一個完整的http的連接(若是前端傳過去不是完整的,就須要Native補齊),這個也會封裝到前端底層造成一個語法糖:

1 this.forward('flight/detail', {id: 1})
2 //==>
3 requestHybrid({
4     tagname: 'forward',
5     param: {
6         topage: 'http://domain.com/webapp/flight/detail.html?id=1',
7         type: 'h5'
8     }
9 });

這個是針對線上的場景,而若是讀取的是內嵌資源的話就不是這麼回事了,好比以前的代碼:

1 requestHybrid({
2     tagname: 'forward',
3     param: {
4         topage: 'flight/detail.html?id=1',
5         type: 'h5'
6     }
7 });

這個是告訴Native去靜態資源flight目錄找尋detail.html而後載入,這裏又涉及到一個問題是:業務到底該怎麼寫代碼?由於不少時候咱們是一套H5代碼瀏覽器與Native兩邊運行,而我若是在H5想從首頁到列表頁直接這樣寫就好了:

this.forward('list', {id: 1})

業務是毫不但願這個代碼要由於接入Hybrid而改變,就算業務開發接受,也會由於跳轉選擇而致使業務混亂引起各類問題,因此前端框架層要解決這個問題,保證業務最小程度的犯錯概率,上面之因此不傳完整http的連接給Native,是由於會有靜態資源內嵌Native的場景,請看下面的例子:

requestHybrid({
    tagname: 'forward',
    param: {
        //Native首先檢查本地有沒有這個文件,若是有就直接讀取本地,沒有就走http
        topage: 'flight/index.html',
        type: 'native'
    }
});

這裏爲解決快速渲染提出了第一個約定:

跳轉時,須要Native去解析URL,判斷是否有本地文件,若是有則加載本地文件

舉個例子來講:

http://domain.com/webapp/flight/index.html
//解析url得出關鍵詞
//===>
flight/index.html
檢查本地是否有該文件,有便直接返回

有這個規則的話,就能夠最大程度上保證業務代碼的一致性,而讀取本地文件也能大大提升性能,緩存這塊咱們後面再說。

history錯亂

前面說了History錯亂的緣由通常來講是由於使用了forward模擬back回退,這種業務場景常常發生:

① 訂單填寫頁須要去支付頁面,因爲一些特殊業務需求,須要通過一箇中間頁作處理,而後再進入真正的支付頁,這個時候支付頁點擊後退事實上是執行的forward的操做(由於點擊回退就到中間頁了)

② 發佈產品的時候會有發佈1->發佈2->發佈預覽->完成->產品詳情頁,這個時候點擊產品詳情頁的後退,咱們也不會但願他回到發佈預覽頁,而是首頁

③ 有可能用戶直接由瀏覽器直接打開APP進入產品詳情頁,這個時候點擊後退是沒有任何記錄的,固然也是回到首頁了。

以上按照業務邏輯的知識的話是正確的,可是以第三種狀況來講,若是回到首頁後再次點擊後退,而首頁的後退又恰好是back操做,那麼會回到產品詳情頁(事實上用戶是想退出該頻道),而更加不妙的是用戶再次點擊產品詳情的回退又回到了首頁,造成了一個死循環!!!

history錯亂,暫時沒有很好的處理辦法,咱們要作的是一旦發現可能會發生history錯亂的頻道就都不要使用back了,好比上面首頁back就寫死回到app大首頁

固然,有些頁面也不是無規律的亂跳的,因此咱們新開一個頁面的時候須要讓新開頁面知道以前是哪一個頁面,若是單頁應用卻是能夠寫在實例對象上,可是一刷新就丟了,因此比較靠譜的作法也許是帶在url上,這個在新開webview的場景下是不可避免的,好比:

//從a頁面進入b頁面
this.forward('b');
//b頁面的實例
this.refer == 'a' //true
//由於頁面刷新會丟失這個管理,因此咱們將這個關聯寫在url上
//b的url webapp/project/b.html?refer=a

Header組件

H5開發中對Header部分的操做是不可避免的,因而咱們抽象出了UIHeader組件處理這種操做,事實上在Hybrid中的Header也應該是一個通用組件,前端作的僅僅是根據約定的格式去調用這個組件便可,可是由於要保證H5與Native調用的一致性,因此要規範化業務代碼的使用,通常的使用方法爲:

 1 //Native以及前端框架會對特殊tagname的標識作默認回調,若是未註冊callback,或者點擊回調callback無返回則執行默認方法
 2 //back前端默認執行History.back,若是不可後退則回到指定URL,Native若是檢測到不可後退則返回Naive大首頁
 3 //home前端默認返回指定URL,Native默認返回大首頁
 4 this.header.set({
 5     left: [
 6         {
 7             //若是出現value字段,則默認不使用icon
 8             tagname: 'back',
 9             value: '回退',
10             //若是設置了lefticon或者righticon,則顯示icon
11             //native會提供經常使用圖標icon映射,若是找不到,便會去當前業務頻道專用目錄獲取圖標
12             lefticon: 'back',
13             callback: function () { }
14         }
15     ],
16     right: [
17         {
18             //默認icon爲tagname,這裏爲icon
19             tagname: 'search',
20             callback: function () { }
21         },
22     //自定義圖標
23         {
24             tagname: 'me',
25             //會去hotel頻道存儲靜態header圖標資源目錄搜尋該圖標,沒有便使用默認圖標
26             icon: 'hotel/me.png',
27             callback: function () { }
28         }
29     ],
30     title: 'title',
31     //顯示主標題,子標題的場景
32     title: ['title', 'subtitle'],
33 
34     //定製化title
35     title: {
36         value: 'title',
37         //標題右邊圖標
38         righticon: 'down', //也能夠設置lefticon
39         //標題類型,默認爲空,設置的話須要特殊處理
40         //type: 'tabs',
41         //點擊標題時的回調,默認爲空
42         callback: function () { }
43     }
44 });

由於通常來講左邊只有一個返回相關的按鈕,因此會提供一個語法糖(在底層依舊會還原爲上面的形式):

 1 this.header.set({
 2     left: [{
 3         tagname: 'back',
 4         callback: function(){}
 5     }],
 6     title: '',
 7 });
 8 //語法糖=>
 9 this.header.set({
10     back: function () { },
11     title: ''
12 });

圖標

header組件上會有不少的圖標,而根據以前的約定,tagname與圖標是一一對應的,這裏就要給出一些基本的映射關係了:

由於H5與native是以tagname做爲標識,因此必定不能重複

這些皆須要Native同事實現,若是是新出的圖標的話,能夠讀取線上http的圖標,好比這樣:

 1 Hybrid.ui.header.set({
 2     back: function () {
 3         requestHybrid({
 4             tagname: 'back',
 5             param: {
 6                 topage: 'index',
 7                 type: 'native'
 8             }
 9         });
10     },
11     title: '讀取線上資源',
12     right: [
13         {
14             tagname: 'search',
15             icon: 'http://images2015.cnblogs.com/blog/294743/201511/294743-20151102143118414-1197511976.png',
16             callback: function () {
17                 alert('讀取線上資源')
18             }
19         }
20     ]
21 });

但若是是經常使用的圖標還要去線上取的話,對性能不太好,而這裏也引出了一個比較大的話題,靜態資源緩存問題,這個咱們在後麪點描述。

防止假死

其實以前我提出過拒絕使用NativeUI的想法,當時最是抵制的就是Header組件,由於若是使用Native的Header的話:

① 咱們的loading將header蓋不住

② 每次前端header有什麼特殊需求都實現不了,必須等待Native支持(好比Header隱藏之類的)

爲了抵制我還提出了一些方案,可是之後面實際項目來講,事實上是很難擺脫Header組件:

① 斷網狀況下白屏問題

② js報錯假死問題

正如所說,咱們會使用Native的功能一個很大的緣由是爲了防止js出錯而致使app假死,而通過咱們以前的設計,連back按鈕的回調也是咱們定義的,若是js報錯的話,這個back的回調可能沒註冊上去也可能回調報錯了,爲了處理這個問題,咱們這裏須要一個約定:

對header的tagname爲back的按鈕作了特殊化,相似可能作特殊化的tagname是home、tel

① 若是back按鈕沒有設置回調則執行webview(瀏覽器)的history.back

② 若是history爲1的話,默認執行退回上一頁

③ 若是點擊back的時候具備回調則執行回調(JavaScript回調,必須返回true)

④ 若是js回調返回true則Native流程結束,若是300ms沒有返回或者返回不爲true則跳轉到大首頁(這個根據業務而定,也可能回到上一頁)

這樣的話,就算js報錯,或者回調報錯,也能夠保證APP不會陷入假死的狀況。

請注意,這樣只能避免用戶進了某一個頁面出不去的狀況,並非說頁面沒BUG!!!
若是這裏發生了阻塞主流程的BUG,頁面應該要有自動預警與在線更改機制,避免用戶&訂單流失

這裏一旦具備回調可是依舊執行了Native回調的場景就必定是頁面有問題,這個時候就應該打點上報日誌,日誌收集後立刻短信轟炸業務開發人員,這個日誌也是有必定要求的:咱們但願錯誤日誌定位到哪個頁面甚至哪個方法出了問題,若是有具體操做路徑就更好了,後面的比較難,第一條必定要作到。當錯誤定位到後,咱們便須要快速解決問題,上線代碼,這裏涉及Hybrid在線更新一塊的邏輯,咱們後面再說。

數據請求

事實上對H5來講,請求走Ajax是沒有問題的,跨域等問題都有不少解決方案,真正讓咱們想用Native代理髮出請求的是帳號信息統一(後面又有Native走tcp的場景),請思考如下場景:

Native每每是能夠持久化登陸信息的,因此不少主流的Hybrid框架若是是直連(webview直接訪問一個url)的話會直接將cookie信息注入給webview,這個時候業務就直接獲取了登陸態了,但總有業務可能會產生登出操做,而後換個帳號登陸進來,這個時候webview與Native的帳號就不統一了,沒有處理方案的話,這個時候用戶就會懵逼了,以爲整個APP不可信!

有一種方案是能夠繞過這個問題的,就是對登陸登出「收口」(咱們又提到收口一詞了哦),限制業務開發登出必須使用APP系統提供的登陸登出,由於通常大公司有統一的passport作鑑權,好比手機百度,就算你在webview中從新登陸了,由於使用的是APP提供的登陸登出,而其餘頻道應用與你皆是使用的passport鑑權,因此能夠用這個方案,可是這個方案對於多數小公司多是不可行的。

第一是不少小公司沒那個意識去打造相似passport這種東西,這個也不是前端能推進的事情,就算你實現了整個passport機制,還得保證整個公司其它團隊使用你的系統,若是有一個團隊不買帳就懵逼了。

沒有統一的帳號系統每每有歷史包袱的因素,技術債須要及時還清

因此如今不少團隊的現狀是一個項目都會有本身的登錄註冊(這個事實上很傻逼),頻道之間登陸態共享都沒有作到,因此對登錄登出作收口便不適用了,可是由於Native是新業務,不存在歷史包袱,APP通常又是戰略性產品,前端作不到的事情,若是和APP掛鉤,每每能夠在某些方面完成相似的事情,就咱們如今來講APP就有本身的一套鑑權機制,雖然不清楚他內部實現(後面需詳細瞭解),可是每一個業務接口對APP都是很友好的,因此請求直接走Native代理髮出會是一個很是好的選擇。

 1 requestHybrid({
 2     //post
 3     tagname: 'get',
 4     param: {
 5         url: 'http://api/demain.com',
 6         param: {a: 1, b: 2}
 7     },
 8     callback: function (data) {
 9     }
10 });

解決了以上問題,事實上只須要Native端新釋放一個接口便可,固然這裏又會回到以前一個問題,post的參數問題,這個時候可能就須要配置爲JavaScriptCore方式通訊,或者將請求參數放在一個全局方法中等待Native調用獲取。

業務開發中須要禁止出現登出操做,全部的登出都要走APP惟一頁面的惟一登出按鈕;若是APP自己未登錄,那麼能夠要求用戶進入頁面前先登錄,也能夠在訪問到具體須要登錄的接口時彈出登錄框讓用戶登錄了才能進行後續操做。

由於請求由native發出不會有跨域問題,考慮到安全性,這裏會有一個域名白名單,只有白名單的請求才能發出去

NativeUI

像咱們前面說的Header組件與登錄框,事實上都算得上Native組件,只不過header是單純的UI組件,登錄框算得上業務組件的,H5會用到NativeUI的場景很少,可是loading這個東西由於要下降頁面白屏時間會常常用到。

通常來講在webview加載html時會有一段時間白屏時間,這個時候便須要Native的loading出場,在頁面加載完成後須要在前端框架層將Native loading關閉。

 1 var HybridUI = {};
 2 HybridUI.showLoading();
 3 //=>
 4 requestHybrid({
 5     tagname: 'showLoading'
 6 });
 7 
 8 HybridUI.showToast({
 9     title: '111',
10     //幾秒後自動關閉提示框,-1須要點擊纔會關閉
11     hidesec: 3,
12     //彈出層關閉時的回調
13     callback: function () { }
14 });
15 //=>
16 requestHybrid({
17     tagname: 'showToast',
18     param: {
19         title: '111',
20         hidesec: 3,
21         callback: function () { }
22     }
23 });

這一套UI組件皆須要與前端框架中的組件使用作到一致性,這種業務類組件很少說,這裏說一個可能會遇到的問題:

NativeUI通訊問題

不可避免的,咱們會遇到NativeUI組件與H5通訊的問題,舉個簡單的例子,咱們爲了交互效果,新開了一Native的彈出層組件,大概這個樣子:

你們這裏不要把它當作單獨的View,將它看作一個H5的彈出層,只不過這個彈出層是Native實現的,整個調用方式也許是這樣的:

 1 requestHybrid({
 2     tagname: 'showCitilist',
 3     param: {
 4         data: [
 5             {name: '北京'}, {name: '上海'}
 6             //......
 7         ]
 8     },
 9     callback: function(item) {
10         alert(item.name)
11     }
12 });

這裏Native彈出了一個彈出層,裝載的是Native的UI,點擊某一個城市,執行的是H5的回調,表面邏輯上這個Native的UI應該是基於Webview的,事實上這個NativeUI多是一個單例,其實這個實現還比較簡單,由於他的點擊交互比較單一,Native能夠很容易的將數據得到再回調H5的方法,這裏與Header上的點擊事件處理一致,比較複雜的是Native新開了一個彈出層而他是一個Webview,裝載咱們本身的H5代碼,這個便複雜了。

Webview通訊

請考慮如下業務場景,此次依舊是使用Native彈出層,可是這裏的彈出層是一個Webview組件,裏面的內容須要咱們自定義,調用多是這樣的:

 1 requestHybrid({
 2    tagname: 'showpagelayer',
 3     param: {
 4         html: '<input id="test" type="text" ><input type="button" id="btn" >',
 5         events: {
 6             'click #btn': function() {
 7                 var v = $('#test').val();
 8                 //調用父元素方法
 9                 //parentCallback(v);
10                 //關閉當前彈出層
11                 //Hybrid.ui.hidepagelayer()
12             }
13         },
14     }
15 });

這個代碼之因此能夠這樣寫,是由於咱們對這個頁面展現的Dom結構與事件有控制力,可是若是這個頁面若是壓根不是我寫的,並且上面那種代碼的應用場景基本爲0,咱們真實的使用場景每每是直接載入一個頁面,好比這個例子:

1 requestHybrid({
2     tagname: 'showpagelayer',
3     param: {
4         src: 'http://domain.com/webapp/common/city.html',
5     }
6 });

若是是以url載入一個頁面的話,咱們對頁面的控制力就沒有了,除非有一個規則讓咱們能夠對頁面的某些方法進行重寫,好比依賴一個框架:

一個好的Hybrid平臺除了基礎實現外,還須要一配套使用前端框架,框架須要最大限度的保證業務代碼一致,提高業務的開發效率

咱們這裏爲了方便你們理解作簡單實現便可。首先,咱們約定,這類能夠用彈出層打開的頁面必定是具有某些「公共」特性的頁面,好比:

① 城市列表頁

② 經常使用聯繫人選擇頁

③ XX類型選擇頁

切記,這類頁面必定是公共業務,不會包含過於業務化的東西,不然是不適用的,那種頁面仍是以url傳參處理吧。

而後,咱們對這類頁面的處理也僅限於回調的處理,不會影響到他們自己的渲染,好比是這樣的頁面:

 1 <input type="text" id="test" >
 2 <input type="button" value="父頁面通訊" id="btn">
 3 <script src="http://sandbox.runjs.cn/uploads/rs/279/2h5lvbt5/zepto.js" type="text/javascript"></script>
 4 <script type="text/javascript">
 5     $('#btn').click(function (){
 6         var val = $('#test').val();
 7         clickAction(val)
 8     });
 9     //override
10     function clickAction (val) {
11         alert(val)
12     };
13 </script>

而咱們真實的調用是這樣的:

 1 requestHybrid({
 2     tagname: 'showpageview',
 3     param: {
 4         src: 'http://sandbox.runjs.cn/show/imbacaz7',
 5         callbacks: {
 6             //請注意,這裏的key值
 7             clickAction: function(val) {
 8                 //parentCallback(val);
 9                 //關閉當前webview,咱們約定這類webview是單例
10                 //Hybrid.ui.hidepageview()
11             }
12         }
13     }
14 });

webview載入結束後,咱們會使用咱們本身定義的方法將原來頁面的方法重寫掉,好比使用JavaScriptCore重寫掉。固然,真實的使用場景不會這麼簡單,具體的業務邏輯就看依賴框架(blade)的實現吧。

PS:這裏的實現過於複雜,不太實用,各位暫時仍是保持url跳轉通訊吧,這裏待研究

靜態資源讀取&更新

前面咱們設置header時,用到了在線靜態資源,那裏直接是使用的http的資源,咱們在實際業務中由於知道本身的圖標在什麼位置因此代碼多是這樣的:

1 {
2     tagname: 'search',
3     //若是當前是機票頻道,這個會轉化爲 http://domain.com/webapp/flight/static/hybrid/icon-search.png
4     icon: './static/hybrid/icon-search.png',
5     callback: function () {
6         alert('讀取線上資源')
7     }
8 }

根據以前的規劃,Native中若是存在靜態資源,也是按頻道劃分的:

webapp //根目錄
├─flight
├─hotel //酒店頻道
│  │  index.html //業務入口html資源,若是不是單頁應用會有多個入口
│  │  main.js //業務全部js資源打包
│  │
│  └─static //靜態樣式資源
│      ├─css 
│      ├─hybrid //存儲業務定製化類Native Header圖標
│      └─images
├─libs
│      libs.js //框架全部js資源打包
│
└─static //框架靜態資源樣式文件
    ├─css
    └─images

如何讀取緩存

咱們開始考慮webview讀取Native靜態資源時候想了幾套方案,好比:

icon: 'hotel/icon.png'

這種形式就是業務開發知道Native的hotel有icon.png的靜態資源,便直接Native讀取了,可是後來我以爲這種方案不太好,誰知道哪次更新Native中就沒有這個包了呢?那個時候豈不是代碼就直接報錯了,因此最後咱們決定咱們全部的靜態資源必定要過http,由於:

不少業務最初開發的時候都是直接使用瀏覽器開發或者Native直連url開發,這種時候就能保證全部的靜態資源的地址不會錯

在正式上線後,咱們可能有一部分公共資源內嵌,這個時候便須要必定機制讓Native返回本地文件:

Native會攔截全部的Webview請求,若是發現某個資源請求本地也存在便直接返回

因此這裏的癥結點是Native如何過濾請求,首先,Native只攔截某些域名的請求,由於咱們本地資源都必定會有一個規則,拿到請求後,咱們會匹配這個規則,好比說,咱們會將這個類型的請求映射到本地:

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

Native會直接去flight目錄尋找是否有這個文件,若是有就直接返回了,可是咱們這裏會有一個憂慮點:

這種攔截全部請求的方法再檢查文件是否存在是否會很耗時

由於我並不能確定,因而讓Native同事作了一個實驗,檢查100個文件本地是否存在,耗時都在10ms之內。

關於讀取Native緩存,咱們也可使用前端構建工具直接以頻道爲單位生成一個清單,而後Native就只對清單內的請求作處理,可是這裏會多一步操做,出錯的概率可能增大,考慮的所有攔截的耗損不是很大,因而咱們採用了所有攔截的方案,這裏簡單說下Native的實現方案,具體各位在代碼中去看吧:

實現方案

這裏IOS與Android實現大同小異,這裏直接放出代碼各位本身去研究吧:

PS:這裏是測試時候的代碼,最後實現請看git裏面的

 1 class DogHybirdURLProtocol: NSURLProtocol {
 2 
 3     override class func canInitWithRequest(request: NSURLRequest) -> Bool {
 4         if let url = request.URL?.absoluteString {
 5             if url.hasPrefix(webAppBaseUrl) {
 6                 let str = url.stringByReplacingOccurrencesOfString(webAppBaseUrl, withString: "")
 7                 var tempArray = str.componentsSeparatedByString("?")
 8                 tempArray = tempArray[0].componentsSeparatedByString(".")
 9                 if tempArray.count == 2 {
10                     let path = MLWebView().LocalResources + tempArray[0]
11                     let type = tempArray[1]
12                     if let _ = NSBundle.mainBundle().pathForResource(path, ofType: type) {
13                         print("文件存在")
14                         print("path == \(path)")
15                         print("type == \(type)")
16                         return true
17                     }
18                 }
19             }
20         }
21         return false
22     }
23 
24     override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
25         return request
26     }
27 
28     override func startLoading() {
29         dispatch_async(dispatch_get_main_queue()) {
30             if let url = self.request.URL?.absoluteString {
31                 if url.hasPrefix(webAppBaseUrl) {
32                     let str = url.stringByReplacingOccurrencesOfString(webAppBaseUrl, withString: "")
33                     var tempArray = str.componentsSeparatedByString("?")
34                     tempArray = tempArray[0].componentsSeparatedByString(".")
35                     if tempArray.count == 2 {
36                         let path = MLWebView().LocalResources + tempArray[0]
37                         let type = tempArray[1]
38                         let client: NSURLProtocolClient = self.client!
39                         if let localUrl = NSBundle.mainBundle().pathForResource(path, ofType: type) {
40                             var typeString = ""
41                             switch type {
42                             case "html":
43                                 typeString = "text/html"
44                                 break
45                             case "js":
46                                 typeString = "application/javascript"
47                                 break
48                             case "css":
49                                 typeString = "text/css"
50                                 break
51                             case "jpg":
52                                 typeString = "image/jpeg"
53                                 break
54                             case "png":
55                                 typeString = "image/png"
56                                 break
57                             default:
58                                 break
59                             }
60                             let fileData = NSData(contentsOfFile: localUrl)
61                             let url = NSURL(fileURLWithPath: localUrl)
62                             let dataLength = fileData?.length ?? 0
63                             let response = NSURLResponse(URL: url, MIMEType: typeString, expectedContentLength: dataLength, textEncodingName: "UTF-8")
64                             client.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
65                             client.URLProtocol(self, didLoadData: fileData!)
66                             client.URLProtocolDidFinishLoading(self)
67                         }
68                         else {
69                             print(">>>>> 沒找到額 <<<<<")
70                         }
71                     }
72                 }
73             }
74         }
75     }
76     
77     override func stopLoading() {
78         
79     }
80 
81 }
IOS

其實這裏優勢已經很是明顯,業務寫代碼的時候壓根不須要考慮文件是否須要本地讀取,Native也能夠有一個開關輕易的配置哪些頻道須要讀取本地文件:

以咱們的demo爲例,關於業務頻道demo的全部靜態資源所有走線上,有效減小APP包大小,公共文件或者框架文件在APP中所有走本地,由於核心框架通常比較大,這裏能夠提高70%以上的載入速度。

增量更新

有緩存策略就會有更新策略,事實上這裏的更新策略涉及到在線發版等功能,這個工做是很是重的,若是說以前的工做前端與Native就能完成的話,那麼這個工做會有server端的同事參加,還有可能造成一個功能龐大的發佈平臺。

最簡單的模擬,就是每次Native大版本發佈都會有一個版本映射表:

{//業務頻道版本號
  flight: 1.0.0,
  hotel: 1.0.0,
  libs: 1.0.0,
  static: 1.0.0
}

其中每一個,若是某一天咱們發現了機票頻道一個BUG,發佈了一個增量包,那麼機票的版本就會增長:

//bug修復
flight: 1.0.1
//功能發佈
flight: 1.1.0

對於這個版本,後臺數據可可能會有這麼一個映射:

channel ver md5
flight 1.0.0 1245355335
hotel 1.0.1 455ettdggd

每個md5值對應着一個實際存在的增量包,在CDN服務器上,每次APP啓動,就會檢查server端的版本號是否是一致,若是不一致就須要從新拉取zip包,而後更新本地版本號:

這個是比較簡單的場景,以一個頻道爲單位的更新,沒有作到粒度更細,安全性方面通常狀況咱們也沒必要關心有人會篡改你的zip包(好比開發商),在你app流量不大的狀況,沒人有那麼蛋疼,可是咱們要考慮開發人員發佈的zip包在某個環節出了問題的狀況,通常來講,咱們的打包程序會根據每一個文件造成一個md5清單,好比這個樣子的:

Native拿到後會去檢查這個清單全部的文件是否完整,若是不完整就有問題,須要打日誌預警放棄此次更新。

簡單實現

咱們這裏因爲暫時沒有Server端的參與,不能作發佈系統,因此暫時是將版本信息放到了項目根目錄作簡單實現,這裏還包含三個頻道的zip包:

PS:真實場景更復雜更嚴謹

{
  "blade": "1.0.0",
  "static": "1.0.0",
  "demo": "1.0.0"
}

咱們如今把更新作到了這個頁面:

這裏的流程是:

1 點擊檢查更新首先檢查Native裏面有沒有hybrid_ver.json這個文件,沒有就去http://yexiaochai.github.io/Hybrid/webapp/hybrid_ver.json下載,完了拿到json串把對應文件所有下載下來解壓:

{
"blade": "1.0.0",
"static": "1.0.0",
"demo": "1.0.0"
}

對應規則是:

http://yexiaochai.github.io/Hybrid/webapp/blade.zip
http://yexiaochai.github.io/Hybrid/webapp/static.zip
http://yexiaochai.github.io/Hybrid/webapp/demo.zip

PS:這裏真實狀況下其實對應的是md5的壓縮包,咱們這裏不去糾結。

若是第二次你點擊,這個時候本地有hybrid_ver.json文件了,你再去遠端獲取這個文件,對比三個頻道的版本號,這個時候是同樣的,因此不會拉取。

若是咱們改動了demo文件中的某個文件,好比加了一個alert什麼的,這個時候就從新造成一個zip包,而後你把demo的版本號加大,好比這樣:

{
"blade": "1.0.0",
"static": "1.0.0",
"demo": "1.0.1"
}

他就該拉取demo的增量包,再次進入系統的時候便能看到更新了。

結語

與上次文章對比,咱們此次在一些Hybrid設計的細節點把握的更多,但願此文能對準備接觸Hybrid技術的朋友提供一些幫助,關於Hybrid的系列還會有最後一篇實戰類文章介紹,有興趣的朋友持續關注吧,這裏是一些效果圖:

 

微博求粉

最後,個人微博粉絲極其少,若是您以爲這篇博客對您哪怕有一絲絲的幫助,微博求粉博客求贊!!!

相關文章
相關標籤/搜索