精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

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

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

前言

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

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

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

Hybrid發(fā)家史

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

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

場景重現(xiàn)

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

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

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

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

總結(jié)

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

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

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

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

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

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

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

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

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

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

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

Andriod APP二維碼:

代碼地址:

https://github.com/yexiaochai/hybrid

#p#

Native與前端分工

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

交互設(shè)計

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

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

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

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

資源訪問機制

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

賬號信息設(shè)計

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

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

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

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

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

Hybrid交互設(shè)計

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

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

  • ctrip://wireless 打開攜程App

  • weixin:// 打開微信

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

#p#

JS to Native

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

 

  1. BNJS.http.get();//向業(yè)務服務器拿請求據(jù)【1.0】 1.3版本接口有擴展 
  2. BNJS.http.post();//向業(yè)務服務器提交數(shù)據(jù)【1.0】 
  3. BNJS.http.sign();//計算簽名【1.0】 
  4. BNJS.http.getNA();//向NA服務器拿請求據(jù)【1.0】 1.3版本接口有擴展 
  5. BNJS.http.postNA();//向NA服務器提交數(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 : '測試post'
  6.             contact : '18721687903' 
  7.         }, 
  8.         onSuccess : function(res){ 
  9.             alert('發(fā)送post請求成功!'); 
  10.         }, 
  11.         onFail : function(res){ 
  12.             alert('發(fā)送post請求失敗!'); 
  13.         } 
  14.     }); 
  15. }); 

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

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

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

API式交互

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

這里類似的微薄開放平臺的接口是這樣定義的:

粉絲服務(新手接入指南

讀取接口

接收消息

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

寫入接口

發(fā)送消息

向用戶回復私信消息接口 

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

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

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

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

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

格式約定

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

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

所以我這邊與Native約定的請求模型是: 

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

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

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

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

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

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

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

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

代碼實現(xiàn)

這里給一個簡單的代碼實現(xiàn),真實代碼在APP中會有所變化:

  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(); //時間戳,防止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. }; 

因為Native對于H5來是底層,框架&底層一般來說是不會關(guān)注業(yè)務實現(xiàn)的,所以真實業(yè)務中Native調(diào)用H5場景較少,這里不予關(guān)注了。

#p#

常用交互API

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

跳轉(zhuǎn)

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

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

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

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

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

  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某一個頁面可以這樣: 

  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一致,我們甚至會有animattype參數(shù)決定切換頁面時的動畫效果,真實使用時可能會封裝全局方法略去tagname的細節(jié),這時就和糯米對外釋放的接口差不多了。

Header 組件的設(shè)計

最初我其實是抵制使用Native提供的UI組件的,尤其是Header,因為平臺化后,Native每次改動都很慎重并且響應很慢,但是出于兩點核心因素考慮,我基本放棄了抵抗:

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

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

PS:Native吊起Native時,如果300ms沒有響應需要出loading組件,避免白屏

因為H5站點本來就有Header組件,站在前端框架層來說,需要確保業(yè)務的代碼是一致的,所有的差異需要在框架層做到透明化,簡單來說Header的設(shè)計需要遵循:

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

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

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

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

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

③ 滿足一些特殊配置,比如標簽類header

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

  1. //Native以及前端框架會對特殊tagname的標識做默認回調(diào),如果未注冊callback,或者點擊回調(diào)callback無返回則執(zhí)行默認方法 
  2. // back前端默認執(zhí)行History.back,如果不可后退則回到指定URL,Native如果檢測到不可后退則返回Naive大首頁 
  3. // home前端默認返回指定URL,Native默認返回大首頁 
  4. this.header.set({ 
  5.     left: [ 
  6.         { 
  7.             //如果出現(xiàn)value字段,則默認不使用icon 
  8.             tagname: 'back'
  9.             value: '回退'
  10.             //如果設(shè)置了lefticon或者righticon,則顯示icon 
  11.             //native會提供常用圖標icon映射,如果找不到,便會去當前業(yè)務頻道專用目錄獲取圖標 
  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頻道存儲靜態(tài)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'//也可以設(shè)置lefticon 
  39.         //標題類型,默認為空,設(shè)置的話需要特殊處理 
  40.         //type: 'tabs', 
  41.         //點擊標題時的回調(diào),默認為空 
  42.         callback: function () { } 
  43.     } 
  44. }); 

因為Header左邊一般來說只有一個按鈕,所以其對象可以使用這種形式:

  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端的實現(xiàn),這里會新增兩個接口,向Native注冊事件,以及注銷事件:

  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組件的實現(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.             //右邊按鈕必須保持數(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.         //注冊事件,將事件存于本地 
  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,不重置事件,不對header其它地方造成變化,僅僅最簡單的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#

請求類

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

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

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

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

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

常用NativeUI組件

最后,Native會提供幾個常用的Native級別的UI,比如loading加載層,比如toast消息框:

  1. var HybridUI = {}; 
  2. HybridUI.showLoading(); 
  3. //=> 
  4. requestHybrid({ 
  5.     tagname: 'showLoading' 
  6. }); 
  7.  
  8. HybridUI.showToast({ 
  9.     title: '111'
  10.     //幾秒后自動關(guān)閉提示框,-1需要點擊才會關(guān)閉 
  11.     hidesec: 3
  12.     //彈出層關(guān)閉時的回調(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不容易打通,所以在真實業(yè)務開發(fā)過程中,一般只會使用幾個關(guān)鍵的Native UI。

賬號系統(tǒng)的設(shè)計

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

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

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

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

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

以file的方式訪問內(nèi)嵌資源的話,因為API請求控制方為Native,所以鑒權(quán)的工作完全由Native完成,接口訪問如果沒有登錄便彈出 Native級別登錄框引導登錄即可,每次訪問webview將賬號信息種入到webview中,這里有個矛盾點是Native種入webview的時 機,因為有可能是網(wǎng)頁注銷的情況,所以這里的邏輯是:

① webview載入結(jié)束

② Native檢測webview是否包含賬號cookie信息

③ 如果不包含則種入cookie,如果包含則檢測與Native賬號信息是否相同,不同則替換自身

④ 如果檢測到跳到了注銷賬戶的頁面,則需要清理自身賬號信息

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

簡單化賬號接口

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

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

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

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

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

賬號信息獲取

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

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

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

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

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

① 系統(tǒng)登出

② 訪問接口提示需要登錄

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

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

#p#

Hybrid的資源

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

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

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

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

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

增量機制

真實的增量機制需要服務器端的配合,我這里只能簡單描述,Native端會有維護一個版本映射表:

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

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

channel

ver

md5

flight

1.0.0

1245355335

hotel

1.0.1

455ettdggd

 

 

 

當APP啟動時,APP會讀取版本信息,這里發(fā)現(xiàn)hotel的本地版本號比線上的小,便會下載md5對應的zip文件,然后解壓之并且替換整個 hotel文件,本次增量結(jié)束,因為所有的版本文件不會重復,APP回滾時可用回到任意想去的版本,也可以對任意版本做BUG修復。

結(jié)語

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

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

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

2018-02-23 14:44:41

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

2015-06-16 10:44:42

2011-09-06 09:27:15

項目設(shè)計

2009-07-08 09:32:25

Java設(shè)計模式

2014-05-21 15:13:40

AppCanHybrid

2009-02-17 18:17:42

2011-12-26 15:19:20

聚合

2011-11-08 11:21:00

2022-09-20 07:02:20

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

2022-10-09 14:15:42

短鏈設(shè)計

2009-05-04 13:19:27

2016-09-29 12:59:54

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

2011-05-23 11:17:42

2019-08-29 10:21:07

IBM存儲IBM存儲

2009-05-18 09:11:00

IPTV融合寬帶

2009-07-15 15:47:12

JDBC DAO

2021-11-26 07:31:43

Java反射程序

2023-06-01 13:15:23

2019-09-18 08:19:42

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

2023-02-27 09:10:57

前端組件設(shè)計
點贊
收藏

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

亚洲男人的天堂在线视频| 冲田杏梨av在线| 人妻夜夜爽天天爽| 久久久青草婷婷精品综合日韩| 精品一区二区电影| 日本中文字幕二区| 精品精品导航| 国产日韩欧美精品电影三级在线| 91久久精品日日躁夜夜躁国产| 国产性猛交普通话对白| 欧美精品一区二区三区中文字幕| 日韩一区二区精品| 虎白女粉嫩尤物福利视频| 欧美被日视频| 久久一日本道色综合| 成人综合国产精品| 日韩三级一区二区| 欧美日本亚洲韩国国产| 伊人久久免费视频| 你懂得在线视频| 人人九九精品视频| 欧美日韩一区二区三区四区 | 在线免费观看a级片| 色狠狠一区二区三区| 精品国产精品自拍| 黄色一级大片免费| 色开心亚洲综合| 久久婷婷国产综合国色天香| 国产精品白丝jk白祙| 亚洲一区精品在线观看| 国产九九精品| 久久全国免费视频| 国产精品老熟女一区二区| 欧美日韩国产传媒| 亚洲免费电影一区| 亚洲国产精品自拍视频| 中文字幕一区二区三区四区久久 | 天天操精品视频| av在线播放一区| 日韩欧亚中文在线| 国产极品尤物在线| 91九色在线看| 亚洲国产毛片aaaaa无费看| 日本xxx免费| 欧美激情午夜| 国产精品理伦片| 视频一区二区三| www.av在线| 国产女人aaa级久久久级| 看高清中日韩色视频| 天天色棕合合合合合合合| 国产69精品久久99不卡| 亚洲xxxx在线| 亚洲AV午夜精品| 国产精品香蕉一区二区三区| 99在线热播| 亚洲老妇色熟女老太| 国产v日产∨综合v精品视频| 99爱精品视频| 国产刺激高潮av| 成人亚洲精品久久久久软件| 国产精品播放| av女名字大全列表| 久久久亚洲精品一区二区三区| 久久综合九色综合网站| 飘雪影视在线观看免费观看| 久久看人人爽人人| 天堂资源在线亚洲资源| 欧美被日视频| 亚洲一区在线免费观看| 久在线观看视频| 台湾佬成人网| 欧美日韩一区二区在线观看| 人妻换人妻仑乱| 澳门精品久久国产| 国产婷婷成人久久av免费高清| a毛片毛片av永久免费| 欧美亚洲国产激情| 久久久国产一区| 精品亚洲永久免费| 鲁大师成人一区二区三区| 国产精品专区h在线观看| a级片在线免费看| av一区二区三区| 色噜噜狠狠色综合网| 九色porny在线| 亚洲大片免费看| 少妇高清精品毛片在线视频| 日本电影久久久| 亚洲高清不卡av| 中国女人特级毛片| 欧美女人交a| 欧美怡春院一区二区三区| 中文字幕在线播放不卡| 成人app下载| 亚洲高清在线观看一区| 日本一本在线免费福利| 在线区一区二视频| 香蕉视频1024| 精品毛片免费观看| 欧美精品www在线观看| 波多野结衣在线电影| 国产成人免费视频网站| 日韩免费中文专区| cao在线视频| 欧美精品久久天天躁| 91精品人妻一区二区三区蜜桃欧美| 99久久亚洲精品| 97免费视频在线| 97在线播放免费观看| 91麻豆国产香蕉久久精品| 中文字幕超清在线免费观看| 亚洲一区站长工具| 欧美成va人片在线观看| 免费成人深夜蜜桃视频| 99国产精品视频免费观看一公开 | 日韩视频免费在线观看| 亚洲免费在线视频观看| 高清在线不卡av| 亚洲日本一区二区三区在线不卡| 大桥未久在线视频| 欧美一级在线免费| 69xxx免费| 国产精品久久久久毛片大屁完整版 | 九九九久久久久| 免费看黄色91| 欧美日韩亚洲一区二区三区在线观看 | 国产一区2区在线观看| 亚洲免费视频在线观看| 日本三级黄色大片| 高清国产一区二区三区| 一区二区三区国| 国产一区二区三区影视| 亚洲欧美中文日韩在线| 99热在线观看免费精品| 不卡的av中国片| 久久观看最新视频| 国产免费区一区二区三视频免费| 中文字幕日韩综合av| 无码人妻一区二区三区免费| 97精品超碰一区二区三区| 91黄色在线看| 国产精品乱战久久久| 欧美乱大交xxxxx| 99久久夜色精品国产亚洲| 最新欧美精品一区二区三区| 簧片在线免费看| 经典一区二区| 国产精品扒开腿做爽爽爽视频| 亚洲欧洲国产综合| 欧美日韩综合视频| 久久精品一区二区免费播放 | 亚洲欧美一区二区久久| 久久9精品区-无套内射无码| 四虎5151久久欧美毛片| 欧美资源在线观看| 精品电影在线| 91九色最新地址| 久久久久久久久久久久| 日韩不卡在线观看日韩不卡视频| 日韩av一级大片| 欧美激情不卡| 九九久久综合网站| 天堂网在线播放| 色狠狠色狠狠综合| 免费黄色国产视频| 国产精品88av| 国产69精品久久久久999小说| 日韩三区视频| 国产精品久久91| 国产原创在线观看| 欧美成人一区二区| 超碰中文字幕在线| 国产欧美日本一区视频| 手机在线免费毛片| 日韩午夜在线| 亚洲一区二区在线看| 精品中文视频| 97av在线视频| 三级外国片在线观看视频| 欧美一区二区成人6969| 欧美一区二区三区四| 国产精品卡一卡二| 娇妻高潮浓精白浆xxⅹ| 日韩av电影一区| 亚洲精品国产suv一区88| 五月天亚洲色图| 成人福利网站在线观看11| 美女尤物在线视频| 在线午夜精品自拍| 可以免费观看的毛片| 欧美午夜视频网站| 久久久久久av无码免费网站| 国产视频911| 绯色av蜜臀vs少妇| 丝袜美腿一区二区三区| 久久天天东北熟女毛茸茸| 亚洲精品播放| 亚洲自拍偷拍在线| 日韩不卡免费高清视频| 欧美精品一区三区| 岛国在线视频免费看| 精品成人一区二区| 一级特黄aa大片| 香蕉影视欧美成人| 三级影片在线观看| 久久影院午夜片一区| 日本中文字幕精品| 久久成人免费网| 红桃av在线播放| 欧美激情麻豆| 亚洲精品久久区二区三区蜜桃臀| 牛牛影视久久网| 91日本视频在线| 欧美影视资讯| 97在线精品视频| 污污的网站在线免费观看| 中文字幕精品视频| 黄色av免费在线观看| 亚洲福利精品在线| 国产黄色片免费| 欧美日韩你懂的| 国产精品免费无遮挡无码永久视频| 亚洲综合精品自拍| 手机在线免费看毛片| 亚洲国产精品黑人久久久| 人妻少妇一区二区| 成人黄色777网| 911亚洲精选| 国产剧情一区二区三区| 亚洲精品视频三区| 捆绑紧缚一区二区三区视频| 毛片毛片毛片毛片毛片毛片毛片毛片毛片 | 欧美另类videos| 99视频精品全部免费在线视频| 日本一区二区三区四区高清视频| 卡通动漫国产精品| 国产欧美日韩亚洲| 超碰成人在线免费| 国产精品久久久久久久免费大片| 欧美三级一区| 51国偷自产一区二区三区 | 手机av免费在线| 久久国产精品久久久久久久久久| 欧美人xxx| 久久久999精品视频| 超碰在线最新| 欧美成人免费全部| 91极品在线| 欧美国产精品人人做人人爱| 国产黄色大片在线观看| 久久久久久一区二区三区| a级片在线免费| 国语自产偷拍精品视频偷| av影院在线免费观看| 欧美在线观看网址综合| 日韩性xxx| 国产日韩欧美视频在线| 久久av网站| 国产精品视频免费观看| 精品少妇3p| 色综合久久88色综合天天提莫| 日韩国产在线| 免费看日b视频| 一本色道久久综合一区| 成人精品视频一区二区| 久久国产人妖系列| 黑人巨大猛交丰满少妇| 成人av电影在线观看| 国产激情在线免费观看| 国产精品久久久久久久久免费桃花 | 亚洲视频中文字幕| 91插插插插插插| 亚洲国产精品久久人人爱| 91国产丝袜播放在线| 欧美自拍偷拍一区| 国产精品伦一区二区三区| 精品久久久久久无| 久草在线网址| 久久久91精品国产一区不卡| 丝袜在线观看| 日本在线观看天堂男亚洲| 欧美一级网址| 国产chinese精品一区二区| 少妇精品久久久一区二区| 一区二区三区久久网| 亚洲欧洲一区二区天堂久久| 九色porny91| 国产成人综合亚洲91猫咪| 亚洲成人日韩在线| 亚洲欧美激情插| 天天操夜夜操视频| 欧美一区二区三级| 你懂的在线免费观看| 欧美插天视频在线播放| 伊伊综合在线| 亚洲一区二区三区视频| 蜜桃成人av| 成人区一区二区| 日本不卡在线视频| 人妻体内射精一区二区三区| 亚洲国产精品ⅴa在线观看| 日本熟妇色xxxxx日本免费看| 欧美日韩国产中文| 青青草视频在线观看| 欧美丰满少妇xxxxx做受| 精品日本视频| 美脚丝袜一区二区三区在线观看| 欧美一区亚洲| www.99av.com| 久久看人人爽人人| 九热这里只有精品| 91精品啪在线观看国产60岁| 理论视频在线| 午夜精品久久久久久久99黑人| 99er精品视频| 亚洲欧美久久久久一区二区三区| 国产一区二区你懂的| 亚洲一区和二区| 中文字幕一区二| 中文字幕第31页| 亚洲跨种族黑人xxx| 136福利第一导航国产在线| 亚洲一区免费网站| 成人a'v在线播放| 日本成年人网址| 99视频精品免费视频| 久青草视频在线观看| 欧美一激情一区二区三区| 免费a级在线播放| 国产精品一区二区三区成人| 久久爱www成人| 亚洲熟女乱色一区二区三区| 成人性生交大合| 欧美极品视频在线观看| 91麻豆精品国产91久久久久 | 欧美激情在线观看视频| 欧美成年网站| 8x8x华人在线| 国产精品一区二区久久精品爱涩| 日本一级特级毛片视频| 欧美精品 国产精品| 久久综合之合合综合久久| 国产在线观看一区二区三区 | 最近中文字幕免费观看| 亚洲欧美制服中文字幕| 中文字幕日本一区二区| 天堂√在线观看一区二区| 日本中文字幕一区| 婷婷丁香综合网| 在线成人高清不卡| а天堂中文在线官网| 超碰在线97av| aa国产精品| 亚洲天堂岛国片| 欧美三区在线观看| 国产在线激情| 国产精品播放| 欧美一级播放| 我不卡一区二区| 欧美日韩激情一区二区三区| 黄色小网站在线观看| 99在线影院| 老司机一区二区三区| 中文字幕第69页| 精品日本一线二线三线不卡| 成年女人在线看片| 日韩av电影免费播放| 国精产品一区一区三区mba视频| 农村妇女精品一区二区| 精品国产一区二区精华| 美脚恋feet久草欧美| 一区二区视频在线观看| 国产jizzjizz一区二区| 欧美精品二区三区| 在线播放精品一区二区三区| av在线亚洲一区| 18禁网站免费无遮挡无码中文| 久久久天堂av| 国产一区二区麻豆| 欧美成人激情视频| 欧美日韩一本| jizz大全欧美jizzcom| 亚洲自拍偷拍av| 黄色片视频在线观看| 97久久天天综合色天天综合色hd| 亚洲尤物影院| www.5588.com毛片| 亚洲欧美国产va在线影院| 亚洲男人在线| 国产l精品国产亚洲区久久| 国产精品久99| 亚洲欧美日本在线观看| 91在线高清视频| 六月婷婷一区| 免费无码毛片一区二区app| 国产一区二区久久精品| 这里视频有精品| 午夜宅男在线视频| 丁香五六月婷婷久久激情|