自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

淺談Hybrid技術(shù)的設(shè)計(jì)與實(shí)現(xiàn)

開發(fā) 前端
隨著移動(dòng)浪潮的興起,各種APP層出不窮,極速的業(yè)務(wù)擴(kuò)展提升了團(tuán)隊(duì)對(duì)開發(fā)效率的要求,這個(gè)時(shí)候使用IOS&Andriod開發(fā)一個(gè)APP似乎成本有點(diǎn)過高了,而H5的低成本、高效率、跨平臺(tái)等特性馬上被利用起來形成了一種新的開發(fā)模式:Hybrid APP。

前言

隨著移動(dòng)浪潮的興起,各種APP層出不窮,極速的業(yè)務(wù)擴(kuò)展提升了團(tuán)隊(duì)對(duì)開發(fā)效率的要求,這個(gè)時(shí)候使用IOS&Andriod開發(fā)一個(gè)APP似乎成本有點(diǎn)過高了,而H5的低成本、高效率、跨平臺(tái)等特性馬上被利用起來形成了一種新的開發(fā)模式:Hybrid APP。

作為一種混合開發(fā)的模式,Hybrid APP底層依賴于Native提供的容器(UIWebview),上層使用Html&Css&JS做業(yè)務(wù)開發(fā),底層透明化、上層多多樣化,這種場(chǎng)景非常有利于前端介入,非常適合業(yè)務(wù)快速迭代,于是Hybrid火啦。

本來我覺得這種開發(fā)模式既然大家都知道了,那么Hybrid就沒有什么探討的價(jià)值了,但令我詫異的是依舊有很多人對(duì)Hybrid這種模式感到陌生,這種情況在二線城市很常見,所以我這里嘗試從另一個(gè)方面向各位介紹Hybrid,期望對(duì)各位正確的技術(shù)選型有所幫助。

Hybrid發(fā)家史

最初攜程的應(yīng)用全部是Native的,H5站點(diǎn)只占其流量很小的一部分,當(dāng)時(shí)Native有200人紅紅火火,而H5開僅有5人左右在打醬油,后面 無線團(tuán)隊(duì)來了一個(gè)執(zhí)行力十分強(qiáng)的服務(wù)器端出身的leader,他為了了解前端開發(fā),居然親手使用jQuery Mobile開發(fā)了第一版程序,雖然很快方案便被推翻,但是H5團(tuán)隊(duì)開始發(fā)力,在短時(shí)間內(nèi)已經(jīng)趕上了Native的業(yè)務(wù)進(jìn)度:

突然有一天andriod同事跑過來告訴我們andriod中有一個(gè)方法最大樹限制,可能一些頁面需要我們內(nèi)嵌H5的頁面,于是Native與H5 框架團(tuán)隊(duì)牽頭做了第一個(gè)Hybrid項(xiàng)目,攜程第一次出現(xiàn)了一套代碼兼容三端的情況。這個(gè)開發(fā)效率杠杠的,團(tuán)隊(duì)嘗到了甜頭,于是乎后續(xù)的頻道基本都開始了 Hybrid開發(fā),到我離開時(shí),整個(gè)機(jī)制已經(jīng)十分成熟了,而前端也有幾百人了。

場(chǎng)景重現(xiàn)

狼廠有三大大流量APP,手機(jī)百度、百度地圖、糯米APP,最近接入糯米的時(shí)候,發(fā)現(xiàn)他們也在做Hybrid平臺(tái)化相關(guān)的推廣,將靜態(tài)資源打包至Native中,Native提供js調(diào)用原生應(yīng)用的能力,從產(chǎn)品化和工程化來說做的很不錯(cuò),但是有兩個(gè)瑕疵:

① 資源全部打包至Naive中APP尺寸會(huì)增大,就算以增量機(jī)制也避免不了APP的膨脹,因?yàn)楝F(xiàn)在接入的頻道較少一個(gè)頻道500K沒有感覺,一旦平臺(tái)化后主APP尺寸會(huì)急劇增大

② 糯米前端框架團(tuán)隊(duì)封裝了Native端的能力,但是沒有提供配套的前端框架,這個(gè)解決方案是不完整的。很多業(yè)務(wù)已經(jīng)有H5站點(diǎn)了,為了接入還得單獨(dú)開發(fā)一 套程序;而就算是新業(yè)務(wù)接入,又會(huì)面臨嵌入資源必須是靜態(tài)資源的限制,做出來的項(xiàng)目沒有SEO,如果關(guān)注SEO的話還是需要再開發(fā),從工程角度來說是有問 題的。

但從產(chǎn)品可接入度與產(chǎn)品化來說,糯米Hybrid化的大方向是很樂觀的,也確實(shí)取得了一些成績(jī),在短時(shí)間就有很多頻道接入了,隨著推廣進(jìn)行,明年可 能會(huì)形成一個(gè)大型的Hybrid平臺(tái)。但是因?yàn)槲乙步?jīng)歷過推廣框架,當(dāng)聽到他們忽悠我說性能會(huì)提高70%,與Native體驗(yàn)基本一致時(shí),不知為何我居然 笑了......

總結(jié)

如果讀了上面幾個(gè)故事你依舊不知道為何要使用Hybrid技術(shù)的話,我這里再做一個(gè)總結(jié)吧:

Hybrid開發(fā)效率高、跨平臺(tái)、底層本
Hybrid從業(yè)務(wù)開發(fā)上講,沒有版本問題,有BUG能及時(shí)修復(fù)

Hybrid是有缺點(diǎn)的,Hybrid體驗(yàn)就肯定比不上Native,所以使用有其場(chǎng)景,但是對(duì)于需要快速試錯(cuò)、快速占領(lǐng)市場(chǎng)的團(tuán)隊(duì)來說,Hybrid一定是不二的選擇,團(tuán)隊(duì)生存下來后還是需要做體驗(yàn)更好的原生APP

好了,上面扯了那么多沒用的東西,今天的目的其實(shí)是為大家介紹Hybrid的一些設(shè)計(jì)知識(shí),如果你認(rèn)真閱讀此文,可能在以下方面對(duì)你有所幫助:

① Hybrid中Native與前端各自的工作是什么

② Hybrid的交互接口如何設(shè)計(jì)

③ Hybrid的Header如何設(shè)計(jì)

④ Hybrid的如何設(shè)計(jì)目錄結(jié)構(gòu)以及增量機(jī)制如何實(shí)現(xiàn)

 資源緩存策略,白屏問題......

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

然后文中Andriod相關(guān)代碼由我的同事明月提供,這里特別感謝明月同學(xué)對(duì)我的支持,這里掃描二維碼可以下載APP進(jìn)行測(cè)試:

Andriod APP二維碼:

代碼地址:

https://github.com/yexiaochai/hybrid

#p#

Native與前端分工

在做Hybrid架構(gòu)設(shè)計(jì)之前需要分清Native與前端的界限,首先Native提供的是一宿主環(huán)境,要合理的利用Native提供的能力,要實(shí)現(xiàn)通用的Hybrid平臺(tái)架構(gòu),站在前端視角,我認(rèn)為需要考慮以下核心設(shè)計(jì)問題。

交互設(shè)計(jì)

Hybrid架構(gòu)設(shè)計(jì)第一個(gè)要考慮的問題是如何設(shè)計(jì)與前端的交互,如果這塊設(shè)計(jì)的不好會(huì)對(duì)后續(xù)開發(fā)、前端框架維護(hù)造成深遠(yuǎn)的影響,并且這種影響往往是不可逆的,所以這里需要前端與Native好好配合,提供通用的接口,比如:

① NativeUI組件,header組件、消息類組件

② 通訊錄、系統(tǒng)、設(shè)備信息讀取接口

③ H5與Native的互相跳轉(zhuǎn),比如H5如何跳到一個(gè)Native頁面,H5如何新開Webview做動(dòng)畫跳到另一個(gè)H5頁面

資源訪問機(jī)制

Native首先需要考慮如何訪問H5資源,做到既能以file的方式訪問Native內(nèi)部資源,又能使用url的方式訪問線上資源;需要提供前端 資源增量替換機(jī)制,以擺脫APP迭代發(fā)版問題,避免用戶升級(jí)APP。這里就會(huì)涉及到靜態(tài)資源在APP中的存放策略,更新策略的設(shè)計(jì),復(fù)雜的話還會(huì)涉及到服 務(wù)器端的支持。

賬號(hào)信息設(shè)計(jì)

賬號(hào)系統(tǒng)是重要并且無法避免的,Native需要設(shè)計(jì)良好安全的身份驗(yàn)證機(jī)制,保證這塊對(duì)業(yè)務(wù)開發(fā)者足夠透明,打通賬戶信息。

Hybrid開發(fā)調(diào)試

功能設(shè)計(jì)完并不是結(jié)束,Native與前端需要商量出一套可開發(fā)調(diào)試的模型,不然很多業(yè)務(wù)開發(fā)的工作將難以繼續(xù),這個(gè)很多文章已經(jīng)接受過了,本文不贅述。

至于Native還會(huì)關(guān)注的一些通訊設(shè)計(jì)、并發(fā)設(shè)計(jì)、異常處理、日志監(jiān)控以及安全模塊因?yàn)椴皇俏疑婕暗念I(lǐng)域便不予關(guān)注了(事實(shí)上是想關(guān)注不得其門),而前端要做的事情就是封裝Native提供的各種能力,整體架構(gòu)是這樣的:

真實(shí)業(yè)務(wù)開發(fā)時(shí),Native除了會(huì)關(guān)注登錄模塊之外還會(huì)封裝支付等重要模塊,這里視業(yè)務(wù)而定。

Hybrid交互設(shè)計(jì)

Hybrid的交互無非是Native調(diào)用前端頁面的JS方法,或者前端頁面通過JS調(diào)用Native提供的接口,兩者交互的橋梁皆Webview:

app自身可以自定義url schema,并且把自定義的url注冊(cè)在調(diào)度中心, 例如

  • ctrip://wireless 打開攜程App

  • weixin:// 打開微信

我們JS與Native通信一般就是創(chuàng)建這類URL被Native捕獲處理,后續(xù)也出現(xiàn)了其它前端調(diào)用Native的方式,但可以做底層封裝使其透明化,所以重點(diǎn)以及是如何進(jìn)行前端與Native的交互設(shè)計(jì)。

#p#

JS to Native

Native在每個(gè)版本會(huì)提供一些API,前端會(huì)有一個(gè)對(duì)應(yīng)的框架團(tuán)隊(duì)對(duì)其進(jìn)行封裝,釋放業(yè)務(wù)接口。比如糯米對(duì)外的接口是這樣的:

 

  1. BNJS.http.get();//向業(yè)務(wù)服務(wù)器拿請(qǐng)求據(jù)【1.0】 1.3版本接口有擴(kuò)展 
  2. BNJS.http.post();//向業(yè)務(wù)服務(wù)器提交數(shù)據(jù)【1.0】 
  3. BNJS.http.sign();//計(jì)算簽名【1.0】 
  4. BNJS.http.getNA();//向NA服務(wù)器拿請(qǐng)求據(jù)【1.0】 1.3版本接口有擴(kuò)展 
  5. BNJS.http.postNA();//向NA服務(wù)器提交數(shù)據(jù)【1.0】 
  6. BNJS.http.getCatgData();//從Native本地獲取篩選數(shù)據(jù)【1.1】 

 

  1. BNJSReady(function(){ 
  2.     BNJS.http.post({ 
  3.         url : 'http://cp01-testing-tuan02.cp01.baidu.com:8087/naserver/user/feedback'
  4.         params : { 
  5.             msg : '測(cè)試post'
  6.             contact : '18721687903' 
  7.         }, 
  8.         onSuccess : function(res){ 
  9.             alert('發(fā)送post請(qǐng)求成功!'); 
  10.         }, 
  11.         onFail : function(res){ 
  12.             alert('發(fā)送post請(qǐng)求失?。?); 
  13.         } 
  14.     }); 
  15. }); 

前端框架定義了一個(gè)全局變量BNJS作為Native與前端交互的對(duì)象,只要引入了糯米提供的這個(gè)JS庫,并且在糯米封裝的Webview容器中, 前端便獲得了調(diào)用Native的能力,我揣測(cè)糯米這種設(shè)計(jì)是因?yàn)檫@樣便于第三方團(tuán)隊(duì)的接入使用,手機(jī)百度有一款輕應(yīng)用框架也走的這種路線:

clouda.mbaas.account //釋放了clouda全局變量

這樣做有一個(gè)前提是,Native本身已經(jīng)十分穩(wěn)定了,很少新增功能了,否則在直連情況下就會(huì)面臨一個(gè)尷尬,因?yàn)閣eb站點(diǎn)永遠(yuǎn)保持最新的,就會(huì)在一些低版本容器中調(diào)用了沒有提供的Native能力而報(bào)錯(cuò)。

API式交互

手白、糯米底層如何做我們無從得知,但我們發(fā)現(xiàn)調(diào)用Native API接口的方式和我們使用AJAX調(diào)用服務(wù)器端提供的接口是及其相似的:

這里類似的微薄開放平臺(tái)的接口是這樣定義的:

粉絲服務(wù)(新手接入指南

讀取接口

接收消息

接收用戶私信、關(guān)注、取消關(guān)注、@等消息接口 

寫入接口

發(fā)送消息

向用戶回復(fù)私信消息接口 

生成帶參數(shù)的二維碼

生成帶參數(shù)的二維碼接口 

我們要做的就是通過一種方式創(chuàng)建ajax請(qǐng)求即可:

https://api.weibo.com/2/statuses/public_timeline.json

所以我在實(shí)際設(shè)計(jì)Hybrid交互模型時(shí),是以接口為單位進(jìn)行設(shè)計(jì)的,比如獲取通訊錄的總體交互是:

格式約定

交互的第一步是設(shè)計(jì)數(shù)據(jù)格式,這里分為請(qǐng)求數(shù)據(jù)格式與響應(yīng)數(shù)據(jù)格式,參考ajax的請(qǐng)求模型大概是:

$.ajax(options) ⇒ XMLHttpRequest
type (默認(rèn)值:
"GET") HTTP的請(qǐng)求方法(“GET”, “POST”, or other)。
url (默認(rèn)值:當(dāng)前url) 請(qǐng)求的url地址。
data (默認(rèn)值:none) 請(qǐng)求中包含的數(shù)據(jù),對(duì)于GET請(qǐng)求來說,這是包含查詢字符串的url地址,如果是包含的是object的話,$.param會(huì)將其轉(zhuǎn)化成string。

所以我這邊與Native約定的請(qǐng)求模型是: 

  1. requestHybrid({ 
  2.   //創(chuàng)建一個(gè)新的webview對(duì)話框窗口 
  3.   tagname: 'hybridapi'
  4.   //請(qǐng)求參數(shù),會(huì)被Native使用 
  5.   param: {}, 
  6.   //Native處理成功后回調(diào)前端的方法 
  7.   callback: function (data) { 
  8.   } 
  9. }); 

這個(gè)方法執(zhí)行會(huì)形成一個(gè)URL,比如:

hybridschema://hybridapi?callback=hybrid_1446276509894&param=%7B%22data1%22%3A1%2C%22data2%22%3A2%7D

這里提一點(diǎn),APP安裝后會(huì)在手機(jī)上注冊(cè)一個(gè)schema,比如淘寶是taobao://,Native會(huì)有一個(gè)進(jìn)程監(jiān)控Webview發(fā)出的所有 schema://請(qǐng)求,然后分發(fā)到“控制器”hybridapi處理程序,Native控制器處理時(shí)會(huì)需要param提供的參數(shù)(encode過),處 理結(jié)束后將攜帶數(shù)據(jù)獲取Webview window對(duì)象中的callback(hybrid_1446276509894)調(diào)用之

數(shù)據(jù)返回的格式約定是:

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

真實(shí)的數(shù)據(jù)在data對(duì)象中,如果errno不為0的話,便需要提示msg,這里舉個(gè)例子如果錯(cuò)誤碼1代表該接口需要升級(jí)app才能使用的話:

{
  data: {},
  errno:
1,
  msg:
"APP版本過低,請(qǐng)升級(jí)APP版本"
}

代碼實(shí)現(xiàn)

這里給一個(gè)簡(jiǎn)單的代碼實(shí)現(xiàn),真實(shí)代碼在APP中會(huì)有所變化:

  1. window.Hybrid = window.Hybrid || {}; 
  2. var bridgePostMsg = function (url) { 
  3.     if ($.os.ios) { 
  4.         window.location = url; 
  5.     } else { 
  6.         var ifr = $('<iframe style="display: none;" src="' + url + '"/>'); 
  7.         $('body').append(ifr); 
  8.         setTimeout(function () { 
  9.             ifr.remove(); 
  10.         }, 1000
  11.     } 
  12. }; 
  13. var _getHybridUrl = function (params) { 
  14.     var k, paramStr = '', url = 'scheme://'
  15.     url += params.tagname + '?t=' + new Date().getTime(); //時(shí)間戳,防止url不起效 
  16.     if (params.callback) { 
  17.         url += '&callback=' + params.callback; 
  18.         delete params.callback; 
  19.     } 
  20.     if (params.param) { 
  21.         paramStr = typeof params.param == 'object' ? JSON.stringify(params.param) : params.param; 
  22.         url += '&param=' + encodeURIComponent(paramStr); 
  23.     } 
  24.     return url; 
  25. }; 
  26. var requestHybrid = function (params) { 
  27.     //生成唯一執(zhí)行函數(shù),執(zhí)行后銷毀 
  28.     var tt = (new Date().getTime()); 
  29.     var t = 'hybrid_' + tt; 
  30.     var tmpFn; 
  31.  
  32.     //處理有回調(diào)的情況 
  33.     if (params.callback) { 
  34.         tmpFn = params.callback; 
  35.         params.callback = t; 
  36.         window.Hybrid[t] = function (data) { 
  37.             tmpFn(data); 
  38.             delete window.Hybrid[t]; 
  39.         } 
  40.     } 
  41.     bridgePostMsg(_getHybridUrl(params)); 
  42. }; 
  43. //獲取版本信息,約定APP的navigator.userAgent版本包含版本信息:scheme/xx.xx.xx 
  44. var getHybridInfo = function () { 
  45.     var platform_version = {}; 
  46.     var na = navigator.userAgent; 
  47.     var info = na.match(/scheme\/\d\.\d\.\d/); 
  48.  
  49.     if (info && info[0]) { 
  50.         info = info[0].split('/'); 
  51.         if (info && info.length == 2) { 
  52.             platform_version.platform = info[0]; 
  53.             platform_version.version = info[1]; 
  54.         } 
  55.     } 
  56.     return platform_version; 
  57. }; 

因?yàn)镹ative對(duì)于H5來是底層,框架&底層一般來說是不會(huì)關(guān)注業(yè)務(wù)實(shí)現(xiàn)的,所以真實(shí)業(yè)務(wù)中Native調(diào)用H5場(chǎng)景較少,這里不予關(guān)注了。

#p#

常用交互API

良好的交互設(shè)計(jì)是成功的第一步,在真實(shí)業(yè)務(wù)開發(fā)中有一些API一定會(huì)用到。

跳轉(zhuǎn)

跳轉(zhuǎn)是Hybrid必用API之一,對(duì)前端來說有以下跳轉(zhuǎn):

① 頁面內(nèi)跳轉(zhuǎn),與Hybrid無關(guān)

② H5跳轉(zhuǎn)Native界面

③ H5新開Webview跳轉(zhuǎn)H5頁面,一般為做頁面動(dòng)畫切換

如果要使用動(dòng)畫,按業(yè)務(wù)來說有向前與向后兩種,forward&back,所以約定如下,首先是H5跳Native某一個(gè)頁面 

  1. //H5跳Native頁面 
  2. //=>baidubus://forward?t=1446297487682&param=%7B%22topage%22%3A%22home%22%2C%22type%22%3A%22h2n%22%2C%22data2%22%3A2%7D 
  3. requestHybrid({ 
  4.     tagname: 'forward'
  5.     param: { 
  6.         //要去到的頁面 
  7.         topage: 'home'
  8.         //跳轉(zhuǎn)方式,H5跳Native 
  9.         type: 'native'
  10.         //其它參數(shù) 
  11.         data2: 2 
  12.     } 
  13. }); 

比如攜程H5頁面要去到酒店Native某一個(gè)頁面可以這樣: 

  1. //=>schema://forward?t=1446297653344&param=%7B%22topage%22%3A%22hotel%2Fdetail%20%20%22%2C%22type%22%3A%22h2n%22%2C%22id%22%3A20151031%7D 
  2. requestHybrid({ 
  3.     tagname: 'forward'
  4.     param: { 
  5.         //要去到的頁面 
  6.         topage: 'hotel/detail'
  7.         //跳轉(zhuǎn)方式,H5跳Native 
  8.         type: 'native'
  9.         //其它參數(shù) 
  10.         id: 20151031 
  11.     } 
  12. }); 

比如H5新開Webview的方式跳轉(zhuǎn)H5頁面便可以這樣: 

  1. requestHybrid({ 
  2.     tagname: 'forward'
  3.     param: { 
  4.         //要去到的頁面,首先找到hotel頻道,然后定位到detail模塊 
  5.         topage: 'hotel/detail  '
  6.         //跳轉(zhuǎn)方式,H5新開Webview跳轉(zhuǎn),最后裝載H5頁面 
  7.         type: 'webview'
  8.         //其它參數(shù) 
  9.         id: 20151031 
  10.     } 
  11. }); 

back與forward一致,我們甚至?xí)衋nimattype參數(shù)決定切換頁面時(shí)的動(dòng)畫效果,真實(shí)使用時(shí)可能會(huì)封裝全局方法略去tagname的細(xì)節(jié),這時(shí)就和糯米對(duì)外釋放的接口差不多了。

Header 組件的設(shè)計(jì)

最初我其實(shí)是抵制使用Native提供的UI組件的,尤其是Header,因?yàn)槠脚_(tái)化后,Native每次改動(dòng)都很慎重并且響應(yīng)很慢,但是出于兩點(diǎn)核心因素考慮,我基本放棄了抵抗:

① 其它主流容器都是這么做的,比如微信、手機(jī)百度、攜程

② 沒有header一旦網(wǎng)絡(luò)出錯(cuò)出現(xiàn)白屏,APP將陷入假死狀態(tài),這是不可接受的,而一般的解決方案都太業(yè)務(wù)了

PS:Native吊起Native時(shí),如果300ms沒有響應(yīng)需要出loading組件,避免白屏

因?yàn)镠5站點(diǎn)本來就有Header組件,站在前端框架層來說,需要確保業(yè)務(wù)的代碼是一致的,所有的差異需要在框架層做到透明化,簡(jiǎn)單來說Header的設(shè)計(jì)需要遵循:

① H5 header組件與Native提供的header組件使用調(diào)用層接口一致

② 前端框架層根據(jù)環(huán)境判斷選擇應(yīng)該使用H5的header組件抑或Native的header組件

一般來說header組件需要完成以下功能:

① header左側(cè)與右側(cè)可配置,顯示為文字或者圖標(biāo)(這里要求header實(shí)現(xiàn)主流圖標(biāo),并且也可由業(yè)務(wù)控制圖標(biāo)),并需要控制其點(diǎn)擊回調(diào)

② header的title可設(shè)置為單標(biāo)題或者主標(biāo)題、子標(biāo)題類型,并且可配置lefticon與righticon(icon居中)

③ 滿足一些特殊配置,比如標(biāo)簽類header

所以,站在前端業(yè)務(wù)方來說,header的使用方式為(其中tagname是不允許重復(fù)的): 

  1. //Native以及前端框架會(huì)對(duì)特殊tagname的標(biāo)識(shí)做默認(rèn)回調(diào),如果未注冊(cè)callback,或者點(diǎn)擊回調(diào)callback無返回則執(zhí)行默認(rèn)方法 
  2. // back前端默認(rèn)執(zhí)行History.back,如果不可后退則回到指定URL,Native如果檢測(cè)到不可后退則返回Naive大首頁 
  3. // home前端默認(rèn)返回指定URL,Native默認(rèn)返回大首頁 
  4. this.header.set({ 
  5.     left: [ 
  6.         { 
  7.             //如果出現(xiàn)value字段,則默認(rèn)不使用icon 
  8.             tagname: 'back'
  9.             value: '回退'
  10.             //如果設(shè)置了lefticon或者righticon,則顯示icon 
  11.             //native會(huì)提供常用圖標(biāo)icon映射,如果找不到,便會(huì)去當(dāng)前業(yè)務(wù)頻道專用目錄獲取圖標(biāo) 
  12.             lefticon: 'back'
  13.             callback: function () { } 
  14.         } 
  15.     ], 
  16.     right: [ 
  17.         { 
  18.             //默認(rèn)icon為tagname,這里為icon 
  19.             tagname: 'search'
  20.             callback: function () { } 
  21.         }, 
  22.     //自定義圖標(biāo) 
  23.         { 
  24.         tagname: 'me'
  25.         //會(huì)去hotel頻道存儲(chǔ)靜態(tài)header圖標(biāo)資源目錄搜尋該圖標(biāo),沒有便使用默認(rèn)圖標(biāo) 
  26.         icon: 'hotel/me.png'
  27.         callback: function () { } 
  28.     } 
  29.     ], 
  30.     title: 'title'
  31.     //顯示主標(biāo)題,子標(biāo)題的場(chǎng)景 
  32.     title: ['title''subtitle'], 
  33.  
  34.     //定制化title 
  35.     title: { 
  36.         value: 'title'
  37.         //標(biāo)題右邊圖標(biāo) 
  38.         righticon: 'down'//也可以設(shè)置lefticon 
  39.         //標(biāo)題類型,默認(rèn)為空,設(shè)置的話需要特殊處理 
  40.         //type: 'tabs', 
  41.         //點(diǎn)擊標(biāo)題時(shí)的回調(diào),默認(rèn)為空 
  42.         callback: function () { } 
  43.     } 
  44. }); 

因?yàn)镠eader左邊一般來說只有一個(gè)按鈕,所以其對(duì)象可以使用這種形式:

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

為完成Native端的實(shí)現(xiàn),這里會(huì)新增兩個(gè)接口,向Native注冊(cè)事件,以及注銷事件:

  1. var registerHybridCallback = function (ns, name, callback) { 
  2.   if(!window.Hybrid[ns]) window.Hybrid[ns] = {}; 
  3.   window.Hybrid[ns][name] = callback; 
  4. }; 
  5.  
  6. var unRegisterHybridCallback = function (ns) { 
  7.   if(!window.Hybrid[ns]) return
  8.   delete window.Hybrid[ns]; 
  9. }; 

Native Header組件的實(shí)現(xiàn):

  1. define([], function () { 
  2.     'use strict'
  3.  
  4.     return _.inherit({ 
  5.  
  6.         propertys: function () { 
  7.  
  8.             this.left = []; 
  9.             this.right = []; 
  10.             this.title = {}; 
  11.             this.view = null
  12.  
  13.             this.hybridEventFlag = 'Header_Event'
  14.  
  15.         }, 
  16.  
  17.         //全部更新 
  18.         set: function (opts) { 
  19.             if (!opts) return
  20.  
  21.             var left = []; 
  22.             var right = []; 
  23.             var title = {}; 
  24.             var tmp = {}; 
  25.  
  26.             //語法糖適配 
  27.             if (opts.back) { 
  28.                 tmp = { tagname: 'back' }; 
  29.                 if (typeof opts.back == 'string') tmp.value = opts.back; 
  30.                 else if (typeof opts.back == 'function') tmp.callback = opts.back; 
  31.                 else if (typeof opts.back == 'object') _.extend(tmp, opts.back); 
  32.                 left.push(tmp); 
  33.             } else { 
  34.                 if (opts.left) left = opts.left; 
  35.             } 
  36.  
  37.             //右邊按鈕必須保持?jǐn)?shù)據(jù)一致性 
  38.             if (typeof opts.right == 'object' && opts.right.length) right = opts.right 
  39.  
  40.             if (typeof opts.title == 'string') { 
  41.                 title.title = opts.title; 
  42.             } else if (_.isArray(opts.title) && opts.title.length > 1) { 
  43.                 title.title = opts.title[0]; 
  44.                 title.subtitle = opts.title[1]; 
  45.             } else if (typeof opts.title == 'object') { 
  46.                 _.extend(title, opts.title); 
  47.             } 
  48.  
  49.             this.left = left; 
  50.             this.right = right; 
  51.             this.title = title; 
  52.             this.view = opts.view; 
  53.  
  54.             this.registerEvents(); 
  55.  
  56.             _.requestHybrid({ 
  57.                 tagname: 'updateheader'
  58.                 param: { 
  59.                     left: this.left, 
  60.                     right: this.right, 
  61.                     title: this.title 
  62.                 } 
  63.             }); 
  64.  
  65.         }, 
  66.  
  67.         //注冊(cè)事件,將事件存于本地 
  68.         registerEvents: function () { 
  69.             _.unRegisterHybridCallback(this.hybridEventFlag); 
  70.             this._addEvent(this.left); 
  71.             this._addEvent(this.right); 
  72.             this._addEvent(this.title); 
  73.         }, 
  74.  
  75.         _addEvent: function (data) { 
  76.             if (!_.isArray(data)) data = [data]; 
  77.             var i, len, tmp, fn, tagname; 
  78.             var t = 'header_' + (new Date().getTime()); 
  79.  
  80.             for (i = 0, len = data.length; i < len; i++) { 
  81.                 tmp = data[i]; 
  82.                 tagname = tmp.tagname || ''
  83.                 if (tmp.callback) { 
  84.                     fn = $.proxy(tmp.callback, this.view); 
  85.                     tmp.callback = t; 
  86.                     _.registerHeaderCallback(this.hybridEventFlag, t + '_' + tagname, fn); 
  87.                 } 
  88.             } 
  89.         }, 
  90.  
  91.         //顯示header 
  92.         show: function () { 
  93.             _.requestHybrid({ 
  94.                 tagname: 'showheader' 
  95.             }); 
  96.         }, 
  97.  
  98.         //隱藏header 
  99.         hide: function () { 
  100.             _.requestHybrid({ 
  101.                 tagname: 'hideheader'
  102.                 param: { 
  103.                     animate: true 
  104.                 } 
  105.             }); 
  106.         }, 
  107.  
  108.         //只更新title,不重置事件,不對(duì)header其它地方造成變化,僅僅最簡(jiǎn)單的header能如此操作 
  109.         update: function (title) { 
  110.             _.requestHybrid({ 
  111.                 tagname: 'updateheadertitle'
  112.                 param: { 
  113.                     title: 'aaaaa' 
  114.                 } 
  115.             }); 
  116.         }, 
  117.  
  118.         initialize: function () { 
  119.             this.propertys(); 
  120.         } 
  121.     }); 
  122.  
  123. }); 
  124.  
  125. Native Header組件的封裝 

#p#

請(qǐng)求類

雖然get類請(qǐng)求可以用jsonp的方式繞過跨域問題,但是post請(qǐng)求卻是真正的攔路虎,為了安全性服務(wù)器設(shè)置cors會(huì)僅僅針對(duì)幾個(gè)域 名,Hybrid內(nèi)嵌靜態(tài)資源是通過file的方式讀取,這種場(chǎng)景使用cors就不好使了,所以每個(gè)請(qǐng)求需要經(jīng)過Native做一層代理發(fā)出去。

這個(gè)使用場(chǎng)景與Header組件一致,前端框架層必須做到對(duì)業(yè)務(wù)透明化,業(yè)務(wù)事實(shí)上不必關(guān)心這個(gè)請(qǐng)求是由瀏覽器發(fā)出還是由Native發(fā)出:

1 HybridGet = function (url, param, callback) {
2 };
3 HybridPost = function (url, param, callback) {
4 };

真實(shí)的業(yè)務(wù)場(chǎng)景,會(huì)將之封裝到數(shù)據(jù)請(qǐng)求模塊,在底層做適配,在H5站點(diǎn)下使用ajax請(qǐng)求,在Native內(nèi)嵌時(shí)使用代理發(fā)出,與Native的約定為:

  1. requestHybrid({ 
  2.     tagname: 'ajax'
  3.     param: { 
  4.         url: 'hotel/detail'
  5.         param: {}, 
  6.         //默認(rèn)為get 
  7.         type: 'post' 
  8.     }, 
  9.     //響應(yīng)后的回調(diào) 
  10.     callback: function (data) { } 
  11. }); 

常用NativeUI組件

最后,Native會(huì)提供幾個(gè)常用的Native級(jí)別的UI,比如loading加載層,比如toast消息框:

  1. var HybridUI = {}; 
  2. HybridUI.showLoading(); 
  3. //=> 
  4. requestHybrid({ 
  5.     tagname: 'showLoading' 
  6. }); 
  7.  
  8. HybridUI.showToast({ 
  9.     title: '111'
  10.     //幾秒后自動(dòng)關(guān)閉提示框,-1需要點(diǎn)擊才會(huì)關(guān)閉 
  11.     hidesec: 3
  12.     //彈出層關(guān)閉時(shí)的回調(diào) 
  13.     callback: function () { } 
  14. }); 
  15. //=> 
  16. requestHybrid({ 
  17.     tagname: 'showToast'
  18.     param: { 
  19.         title: '111'
  20.         hidesec: 3
  21.         callback: function () { } 
  22.     } 
  23. }); 

Native UI與前端UI不容易打通,所以在真實(shí)業(yè)務(wù)開發(fā)過程中,一般只會(huì)使用幾個(gè)關(guān)鍵的Native UI。

賬號(hào)系統(tǒng)的設(shè)計(jì)

根據(jù)上面的設(shè)計(jì),我們約定在Hybrid中請(qǐng)求有兩種發(fā)出方式:

① 如果是webview訪問線上站點(diǎn)的話,直接使用傳統(tǒng)ajax發(fā)出

② 如果是file的形式讀取Native本地資源的話,請(qǐng)求由Native代理發(fā)出

因?yàn)殪o態(tài)html資源沒有鑒權(quán)的問題,真正的權(quán)限驗(yàn)證需要請(qǐng)求服務(wù)器api響應(yīng)通過錯(cuò)誤碼才能獲得,這是動(dòng)態(tài)語言與靜態(tài)語言做入口頁面的一個(gè)很大的區(qū)別。

以網(wǎng)頁的方式訪問,賬號(hào)登錄與否由是否帶有秘鑰cookie決定(這時(shí)并不能保證秘鑰的有效性),因?yàn)镹ative不關(guān)注業(yè)務(wù)實(shí)現(xiàn),而每次載入都有可能是登錄成功跳回來的結(jié)果,所以每次載入后都需要關(guān)注秘鑰cookie變化,以做到登錄態(tài)數(shù)據(jù)一致性。

以file的方式訪問內(nèi)嵌資源的話,因?yàn)锳PI請(qǐng)求控制方為Native,所以鑒權(quán)的工作完全由Native完成,接口訪問如果沒有登錄便彈出 Native級(jí)別登錄框引導(dǎo)登錄即可,每次訪問webview將賬號(hào)信息種入到webview中,這里有個(gè)矛盾點(diǎn)是Native種入webview的時(shí) 機(jī),因?yàn)橛锌赡苁蔷W(wǎng)頁注銷的情況,所以這里的邏輯是:

① webview載入結(jié)束

② Native檢測(cè)webview是否包含賬號(hào)cookie信息

③ 如果不包含則種入cookie,如果包含則檢測(cè)與Native賬號(hào)信息是否相同,不同則替換自身

④ 如果檢測(cè)到跳到了注銷賬戶的頁面,則需要清理自身賬號(hào)信息

如果登錄不統(tǒng)一會(huì)就會(huì)出現(xiàn)上述復(fù)雜的邏輯,所以真實(shí)情況下我們會(huì)對(duì)登錄接口收口。

簡(jiǎn)單化賬號(hào)接口

平臺(tái)層面覺得上述操作過于復(fù)雜,便強(qiáng)制要求在Hybrid容器中只能使用Native接口進(jìn)行登錄和登出,前端框架在底層做適配,保證上層業(yè)務(wù)的透明,這樣情況會(huì)簡(jiǎn)單很多:

① 使用Native代理做請(qǐng)求接口,如果沒有登錄直接Native層喚起登錄框

② 直連方式使用ajax請(qǐng)求接口,如果沒有登錄則在底層喚起登錄框(需要前端框架支持)

簡(jiǎn)單的登錄登出接口實(shí)現(xiàn): 

  1. /* 
  2. 無論成功與否皆會(huì)關(guān)閉登錄框 
  3. 參數(shù)包括: 
  4. success 登錄成功的回調(diào) 
  5. error 登錄失敗的回調(diào) 
  6. url 如果沒有設(shè)置success,或者success執(zhí)行后沒有返回true,則默認(rèn)跳往此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. //與登錄接口一致,參數(shù)一致 
  20. HybridUI.logout = function () { 
  21. }; 

賬號(hào)信息獲取

在實(shí)際的業(yè)務(wù)開發(fā)中,判斷用戶是否登錄、獲取用戶基本信息的需求比比皆是,所以這里必須保證Hybrid開發(fā)模式與H5開發(fā)模式保持統(tǒng)一,否則需要在業(yè)務(wù)代碼中做很多無謂的判斷,我們?cè)谇岸丝蚣軙?huì)封裝一個(gè)User模塊,主要接口包括:

1 var User = {};
2 User.isLogin = function () { };
3 User.getInfo = function () { };

這個(gè)代碼的底層實(shí)現(xiàn)分為前端實(shí)現(xiàn),Native實(shí)現(xiàn),首先是前端的做法是:

當(dāng)前端頁面載入后,會(huì)做一次異步請(qǐng)求,請(qǐng)求用戶相關(guān)數(shù)據(jù),如果是登錄狀態(tài)便能獲取數(shù)據(jù)存于localstorage中,這里一定不能存取敏感信息

前端使用localstorage的話需要考慮極端情況下使用內(nèi)存變量的方式替換localstorage的實(shí)現(xiàn),否則會(huì)出現(xiàn)不可使用的情況,而后續(xù)的訪問皆是使用localstorage中的數(shù)據(jù)做判斷依據(jù),以下情況需要清理localstorage的賬號(hào)數(shù)據(jù):

① 系統(tǒng)登出

② 訪問接口提示需要登錄

③ 調(diào)用登錄接口

這種模式多用于單頁應(yīng)用,非單頁應(yīng)用一般會(huì)在每次刷新頁面先清空賬號(hào)信息再異步拉取,但是如果當(dāng)前頁面馬上就需要判斷用戶登錄數(shù)據(jù)的話,便不可靠了;處于Hybrid容器中時(shí),因?yàn)镹ative本身就保存了用戶信息,封裝的接口直接由Native獲取即可,這塊比較靠譜。

#p#

Hybrid的資源

目錄結(jié)構(gòu)

Hybrid技術(shù)既然是將靜態(tài)資源存于Native,那么就需要目錄設(shè)計(jì),經(jīng)過之前的經(jīng)驗(yàn),目錄結(jié)構(gòu)一般以2層目錄劃分:

如果我們有兩個(gè)頻道酒店與機(jī)票,那么目錄結(jié)構(gòu)是這樣的: 

  1. webapp //根目錄 
  2. ├─flight 
  3. ├─hotel //酒店頻道 
  4. │  │  index.html //業(yè)務(wù)入口html資源,如果不是單頁應(yīng)用會(huì)有多個(gè)入口 
  5. │  │  main.js //業(yè)務(wù)所有js資源打包 
  6. │  │ 
  7. │  └─static //靜態(tài)樣式資源 
  8. │      ├─css  
  9. │      ├─hybrid //存儲(chǔ)業(yè)務(wù)定制化類Native Header圖標(biāo) 
  10. │      └─images 
  11. ├─libs 
  12. │      libs.js //框架所有js資源打包 
  13. │ 
  14. └─static 
  15.     ├─css 
  16.     └─images 

最初設(shè)計(jì)的forward跳轉(zhuǎn)中的topage參數(shù)規(guī)則是:頻道/具體頁面=>channel/page,其余資源會(huì)由index.html這個(gè)入口文件帶出。

增量機(jī)制

真實(shí)的增量機(jī)制需要服務(wù)器端的配合,我這里只能簡(jiǎn)單描述,Native端會(huì)有維護(hù)一個(gè)版本映射表:

{
  flight:
1.0.0,
  hotel:
1.0.0,
  libs:
1.0.0,
  static:
1.0.0
}

這個(gè)映射表是每次大版本APP發(fā)布時(shí)由服務(wù)器端生成的,如果酒店頻道需要在線做增量發(fā)布的話,會(huì)打包一個(gè)與線上一致的文件目錄,走發(fā)布平臺(tái)發(fā)布,會(huì)在數(shù)據(jù)庫中形成一條記錄:

channel

ver

md5

flight

1.0.0

1245355335

hotel

1.0.1

455ettdggd

 

 

 

當(dāng)APP啟動(dòng)時(shí),APP會(huì)讀取版本信息,這里發(fā)現(xiàn)hotel的本地版本號(hào)比線上的小,便會(huì)下載md5對(duì)應(yīng)的zip文件,然后解壓之并且替換整個(gè) hotel文件,本次增量結(jié)束,因?yàn)樗械陌姹疚募粫?huì)重復(fù),APP回滾時(shí)可用回到任意想去的版本,也可以對(duì)任意版本做BUG修復(fù)。

結(jié)語

github上代碼會(huì)持續(xù)更新,現(xiàn)在界面反正不太好看,大家多多包涵吧,這里是一些效果圖:

Hybrid方案是快速迭代項(xiàng)目,快速占領(lǐng)市場(chǎng)的神器,希望此文能對(duì)準(zhǔn)備接觸Hybrid技術(shù)的朋友提供一些幫助,并且再次感謝明月同學(xué)的配合。

責(zé)任編輯:王雪燕 來源: 博客園
相關(guān)推薦

2018-02-23 14:44:41

負(fù)載均衡技術(shù)分類

2011-09-06 09:27:15

項(xiàng)目設(shè)計(jì)

2009-07-08 09:32:25

Java設(shè)計(jì)模式

2015-06-16 10:44:42

2011-12-26 15:19:20

聚合

2009-02-17 18:17:42

2014-05-21 15:13:40

AppCanHybrid

2022-10-09 14:15:42

短鏈設(shè)計(jì)

2016-09-29 12:59:54

大數(shù)據(jù)采集系統(tǒng)

2009-05-04 13:19:27

2011-11-08 11:21:00

2022-09-20 07:02:20

網(wǎng)絡(luò)爬蟲反爬蟲

2019-08-29 10:21:07

IBM存儲(chǔ)IBM存儲(chǔ)

2009-05-18 09:11:00

IPTV融合寬帶

2019-09-18 08:19:42

DDLMySQL數(shù)據(jù)庫

2023-02-27 09:10:57

前端組件設(shè)計(jì)

2009-07-15 15:47:12

JDBC DAO

2011-05-23 11:17:42

2009-07-07 15:57:29

JSP購物車

2009-06-04 08:01:25

Struts2攔截器原理
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)