談一談前端多容器(多webview平臺)處理方案
來自: http://www.cnblogs.com/yexiaochai/p/5204847.html
文中是我個人的一些開發經驗,希望對各位有用,也希望各位 多多支持討論 ,指出文中 不足 以及提出您的一些 建議 。
雙容器
得益于近幾年移動端的發展,前端早已今非昔比,從大型框架來說angularJS、react、VueJS都有其應用場景,從工程化來說各種配套構建工具也紛紛出世,而從前端復雜度來說,最近幾年的前端代碼難度著實提升不少,從模塊化的必須,到MVC的必要、再到組件化編程,一種分而治之的思想逐漸侵入前端領域,而這種種跡象均表明一個問題,前端代碼現在不好寫了!!!
拋開近幾年前端交互加重而導致的難度,我們今天主要探討下前端跨平臺一塊的痛點,也就是Hybrid多容器解決方案。
Hybrid是一種混合開發模式,最簡單的理解就是,Native會提供一個webview容器(確實不明白可以理解為iframe),然后在里面加載你的H5站點。
在大約三年前,當時Hybrid平臺還比較少,如果一個公司前端團隊比較強的話可以做到一套代碼三端運行就很不錯了,也就是一個H5頁面同時運行在:
① 瀏覽器
② 公司IOS APP Webview容器
③ APP Andriod Webview容器
再這里有個和簡單iframe不同的是,處于Native中的話,那么很多H5的表現便不太一樣了,比如header一部分的UI是Native的,比如獲取定位信息直接由Native給H5,在這里面會有些差異化處理,一般來說只有保持應用層API一致,底層稍作修改即可;但也有一些特殊場景需要判斷,比如,一個按鈕的回調在H5站點的處理和處于Native中不一樣,這個時候可能就需要if else判斷處理了。
總的來說,雙容器時代持續了一陣子,而因為條件仍然比較單一,無非只是判斷H5站點或者自身APP容器,所以問題也就不大。
多容器
量變到一定階段便不再一樣了,簡單從攜程來說,Hybrid的頻道從最初的一個發展到現在APP中80%都是Hybrid頻道,這個時候攜程APP本身有一套完整的Hybrid交互規范,這個時候攜程APP已經不再簡單是個APP了,而是一個Hybrid平臺,開發規范一旦制定,一旦進入工廠化開發就很難更改了,除了攜程各個業務團隊依賴這個APP外,還有很多攜程子公司乃至第三方公司依賴這個APP,那么這個時候底層若是不穩定,那么導致的問題將是連鎖的、不可控的。
這種平臺化的APP產品遠不止攜程一家,已知的就有:
① 微信APP平臺
② 淘寶APP平臺
③ 手機百度APP平臺
④ 糯米平臺
⑤ 手機QQ平臺
......
國內這些“平臺”都有各自問題,不論是微信一些版本不支持flex、手機百度IOS、Andriod Webview容器各種不一致,還是糯米Native默認后退不處理導致假死,都可以看出為了搶占市場,各個團隊走的太急,考慮的應用場景過少,推出產品后后宣傳網站寫的漂亮,API看似豐富,但是光鮮的只是表面,真正形成平臺后,各個業務方接入便會形成各種小概率場景,而Native發版是無力的,Native不動就只能業務開發代碼適配,這個時候受苦的總是各個接入方,而導致罵聲一片。
各個平臺不穩定、考慮場景太少也無可厚非,畢竟Hybrid才火不到幾年,各個公司真正的經驗場景又很難被其它公司吸收,所以這種現象還得持續一段時間......
當然,APP底層的問題不是我們今天思考的重點,我們還是回到前端應用層。
多容器與前端
上述平臺產品雖然有各自的問題,但是其流量優勢是無可比擬的!所以很多業務方、第三方公司都會接入,對于前端來說難度便增加了不少,以百度為例:
最初是前端代碼運行在瀏覽器即可,而現在一套前端代碼卻需要運行在:
① 瀏覽器
② 自身APP
③ 百度地圖APP
④ 手機百度APP
⑤ 糯米APP
而各個APP平臺的Hybrid交互又完全不一致,更有甚者后期還需要微信APP、手機QQ等Hybrid平臺,那么就簡單一個按鈕的交互都會令人頭疼的!因為我們的代碼中可能會出現這種東東:
1 if (shoujibaidu) { 2 //手機百度邏輯 3 4 } else if (baiduditu) { 5 //百度地圖邏輯 6 7 } else if (nuomi) { 8 //糯米邏輯 9 } 10 //......其它平臺邏輯
這種代碼十分令人頭疼,所以我們一般會封裝一個方法在底層,哪個平臺有差異就做特殊處理:
1 hybridCallback({ 2 //默認回調 3 callback: function() { 4 }, 5 //手機百度回調 6 shoubaicallback: function () { 7 }, 8 //...... 9 });
這個方法就是用于處理Hybrid差異而生,只有處于某一個環境,才會執行其中的回調,這其實只是一個語法糖,將判斷的邏輯封裝了,所以這個方案依舊很爛,如果哪天如果你要多一個容器或者少一個容器,你整個站點的代碼要如何處理呢?如果代碼量超過萬行,這個代碼可不好處理!
更好的解決方案是抽離共性,是繼承,一般來說,Hybrid還是有一個很大的特點: 主要邏輯與H5一致 ,一些差異往往是顯示什么,不顯示什么(比如糯米中不顯示H5推薦下載APP的廣告),更多的是一些點擊回調的響應,于是我們找到了更好的方案:
多容器解決方案
容器判斷
解決多容器的第一步是容器判斷,一般來說,不同的Webview容器會有不同的userAgent:
//微信中UA為: Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Mobile/11D257 MicroMessenger/6.1.5 NetType/WIFI //瀏覽器中為: Mozilla/5.0 (iPhone; CPU iPhone OS 7_1_2 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D257 Safari/9537.53 //糯米 Mozilla/5.0 (iPhone; CPU iPhone OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13D15 BDNuomiAppIOS
手機百度也會包含關鍵字:bdbox_x.x(x.x一般是版本號),根據ua我們可以知道當前處于什么環境(ios還是Andriod)與什么平臺。
前端實現
如果是頁面片的開發模式,一個頁面往往會有一個js文件,做的好的團隊這個js文件會是一個類,通過requireJS可以輕易拿到該文件,我們這里不做無用功,直接在之前代碼的基礎上做,有疑問的朋友請移步該文章:
在上文中,我們將一個個頁面以組件化的方式打散了,我們這里新增一個index頁面,并且新增一個按鈕,點擊按鈕彈出一個提示:
1 define([ 2 'AbstractView', 3 'text!IndexPath/tpl.layout.html' 4 ], function ( 5 AbstractView, 6 layoutHtml 7 ) { 8 return _.inherit(AbstractView, { 9 propertys: function ($super) { 10 $super(); 11 this.template = layoutHtml; 12 this.events = { 13 'click .js_clickme': 'clickAction' 14 }; 15 }, 16 17 clickAction: function () { 18 this.showMessage('顯示消息'); 19 }, 20 21 initHeader: function (name) { 22 var title = '多Webview容器'; 23 this.header.set({ 24 view: this, 25 title: title, 26 back: function () { 27 console.log('回退'); 28 } 29 }); 30 } 31 }); 32 }); View Code
1 propertys: function ($super) { 2 $super(); 3 this.template = layoutHtml; 4 this.events = { 5 'click .js_clickme': 'clickAction' 6 }; 7 }, 8 9 clickAction: function () { 10 this.showMessage('顯示消息'); 11 },
首先我們看看這個回調,假如我們需要做到在糯米容器中使用Native的彈出提示的話,代碼便有所不同了:
我們使用的應該是:
1 /** 2 * 使用BNJS之前,必須聲明如下BNJSReady函數,確保BNJS相關屬性信息及頁面加載準備就緒 3 * BNJSReady直接復制使用,請勿改動 4 */ 5 var BNJSReady = function (readyCallback) { 6 if(readyCallback && typeof readyCallback == 'function'){ 7 if(window.BNJS && typeof window.BNJS == 'object' && BNJS._isAllReady){ 8 readyCallback(); 9 }else{ 10 document.addEventListener('BNJSReady', function() { 11 readyCallback(); 12 }, false) 13 } 14 } 15 }; 16 17 BNJSReady(function(){ 18 19 // 顯示確定和取消按鈕 20 BNJS.ui.dialog.show({ 21 title: '測試Dialog', 22 message: '我是測試Dialog~~~~', 23 ok: '確定', 24 cancel: '取消', 25 onConfirm: function() { 26 BNJS.ui.toast.show('您剛剛點擊了確定按鈕'); 27 }, 28 onCancel: function() { 29 BNJS.ui.toast.show('您剛剛點擊了取消按鈕'); 30 } 31 }); 32 33 // 僅顯示'ok'按鈕 34 BNJS.ui.dialog.show({ 35 title: '測試Dialog', 36 message: '我是測試Dialog~~~~', 37 ok: 'ok', 38 onConfirm: function() { 39 BNJS.ui.toast.show('您剛剛點擊了ok按鈕'); 40 } 41 }); 42 43 }); View Code
1 // 僅顯示'ok'按鈕 2 BNJS.ui.dialog.show({ 3 title: '測試Dialog', 4 message: '我是測試Dialog~~~~', 5 ok: 'ok', 6 onConfirm: function() { 7 BNJS.ui.toast.show('您剛剛點擊了ok按鈕'); 8 } 9 });
于是我們在index目錄中新增了一個nuomi.index.js的文件,繼承自index.js,并且在入口文件main_webviews(原main.js文件)中做更改:
1 define([ 2 'IndexPath/index' 3 ], function ( 4 IndexView 5 ) { 6 return _.inherit(IndexView, { 7 8 clickAction: function () { 9 BNJS.ui.dialog.show({ 10 title: '測試Dialog', 11 message: '我是測試Dialog~~~~', 12 ok: 'ok', 13 onConfirm: function () { 14 BNJS.ui.toast.show('您剛剛點擊了ok按鈕'); 15 } 16 }); 17 } 18 19 }); 20 });
如此,在一般瀏覽器中點擊按鈕便是H5的UI組件,在糯米中便是使用的糯米組件了,如果哪天不需要糯米這個平臺將nuomi.js刪除即可:
可以看到,按鈕的點擊已經不一樣了,當然還有很多不足,比如糯米中header部分便沒有做處理。
header組件
header這種組件與上述問題又不一致,這種不一致主要體現在兩個方面:
① 由于底層實現問題,做不到一致
比如手機百度就不支持返回按鈕定制,就連最簡單的title改變都是直接監聽的document.title的變化,并且Andriod還有BUG,像這種底層實現直接就抹殺的基本沒法,一般來說就是把原來的header換個方式顯示在頁面中,可以是弧形按鈕,可以是其它方式。
② header是系統級別的操作,不應該由用戶控制
如同該文中對header組件的處理: 淺談Hybrid技術的設計與實現 ,像header這一類組件,這類組件必須滿足在H5站點與Hybrid中API使用一致,而底層實現各異,與之前不同的是,這里的header組件要考慮的可不止2個平臺那種問題了,他可能是這樣的:
ui.eader //H5站點使用 nuomi.ui.header //糯米使用 xx.ui.header //......
我們這里將場景變小,暫時只考慮糯米與H5的實現,于是會在底層多出一個header的實現:
我這里工作做的多一些,考慮了微信時候的場景,但是這里業務代碼暫時只考慮糯米,對應糯米的文檔:
1 define([], function () { 2 'use strict'; 3 4 return _.inherit({ 5 6 propertys: function () { 7 }, 8 9 //全部更新 10 set: function (opts) { 11 if (!opts) return; 12 var i, len, item; 13 14 var scope = opts.view || this; 15 16 //處理返回邏輯 17 if (opts.back && typeof opts.back == 'function') { 18 BNJS.page.onBtnBackClick({ 19 callback: $.proxy(opts.back, scope) 20 }); 21 } else { 22 23 BNJS.page.onBtnBackClick({ 24 callback: function () { 25 if (history.length > 0) 26 history.back(); 27 else 28 BNJS.page.back(); 29 } 30 }); 31 } 32 33 //處理title 34 if (typeof opts.title == 'string') { 35 BNJS.ui.title.setTitle(opts.title); 36 } 37 38 //刪除右上角所有按鈕【1.3】 39 //每次都會清理右邊所有的按鈕 40 BNJS.ui.title.removeBtnAll(); 41 42 //處理右邊按鈕 43 if (typeof opts.right == 'object' && opts.right.length) { 44 for (i = 0, len = opts.right.length; i < len; i++) { 45 item = opts.right[i]; 46 BNJS.ui.title.addActionButton({ 47 tag: _.uniqueId(), 48 text: item.value, 49 callback: $.proxy(item.callback, scope) 50 }); 51 } 52 } 53 }, 54 55 show: function () { 56 57 }, 58 59 hide: function () { 60 61 }, 62 63 //只更新title 64 update: function (title) { 65 66 }, 67 68 initialize: function () { 69 //隱藏H5頭 70 $('#headerview').hide(); 71 this.propertys(); 72 } 73 74 }); 75 76 }); View Code
代碼實現很簡單,只要保持與H5使用API一致即可,這個時候再簡單改下入口文件,便能適配了。
PS:注意,這里的適配只是簡單實現,考慮多場景的話不能這樣寫代碼!!!
于是我們在糯米中便能很好的運行了
結語
代碼地址
https://github.com/yexiaochai/mvc
demo地址
http://yexiaochai.github.io/mvc/webapp/bus/index.html
測試糯米時請掃描第二個二維碼:
這里拋出了前端多Webview容器會遇到的一些問題,并提出了一個解決思路,后續可能會有更加完整解決方案與demo出來,希望對各位有用,若是有已經涉及到這塊業務的朋友可以私下交流下。