文中是我我的的一些開發經驗,但願對各位有用,也但願各位多多支持討論,指出文中不足以及提出您的一些建議。html
得益於近幾年移動端的發展,前端早已今非昔比,從大型框架來講angularJS、react、VueJS都有其應用場景,從工程化來講各類配套構建工具也紛紛出世,而從前端複雜度來講,最近幾年的前端代碼難度着實提高很多,從模塊化的必須,到MVC的必要、再到組件化編程,一種分而治之的思想逐漸侵入前端領域,而這種種跡象均代表一個問題,前端代碼如今很差寫了!!!前端
拋開近幾年前端交互加劇而致使的難度,咱們今天主要探討下前端跨平臺一塊的痛點,也就是Hybrid多容器解決方案。react
Hybrid是一種混合開發模式,最簡單的理解就是,Native會提供一個webview容器(確實不明白能夠理解爲iframe),而後在裏面加載你的H5站點。ios
在大約三年前,當時Hybrid平臺還比較少,若是一個公司前端團隊比較強的話能夠作到一套代碼三端運行就很不錯了,也就是一個H5頁面同時運行在:git
① 瀏覽器github
② 公司IOS APP Webview容器web
③ APP Andriod Webview容器編程
再這裏有個和簡單iframe不一樣的是,處於Native中的話,那麼不少H5的表現便不太同樣了,好比header一部分的UI是Native的,好比獲取定位信息直接由Native給H5,在這裏面會有些差別化處理,通常來講只有保持應用層API一致,底層稍做修改便可;但也有一些特殊場景須要判斷,好比,一個按鈕的回調在H5站點的處理和處於Native中不同,這個時候可能就須要if else判斷處理了。api
總的來講,雙容器時代持續了一陣子,而由於條件仍然比較單一,無非只是判斷H5站點或者自身APP容器,因此問題也就不大。瀏覽器
量變到必定階段便再也不同樣了,簡單從攜程來講,Hybrid的頻道從最初的一個發展到如今APP中80%都是Hybrid頻道,攜程APP自己有一套完整的Hybrid交互規範,儼然已經再也不簡單是個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 });
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 });
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這種組件與上述問題又不一致,這種不一致主要體如今兩個方面:
① 因爲底層實現問題,作不到一致
好比手機百度就不支持返回按鈕定製,就連最簡單的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 });
代碼實現很簡單,只要保持與H5使用API一致便可,這個時候再簡單改下入口文件,便能適配了。
PS:注意,這裏的適配只是簡單實現,考慮多場景的話不能這樣寫代碼!!!
因而咱們在糯米中便能很好的運行了
https://github.com/yexiaochai/mvc
http://yexiaochai.github.io/mvc/webapp/bus/index.html
測試糯米時請掃描第二個二維碼:
這裏拋出了前端多Webview容器會遇到的一些問題,並提出了一個解決思路,後續可能會有更加完整解決方案與demo出來,但願對各位有用,如果有已經涉及到這塊業務的朋友能夠私下交流下。