文中是我個人的一些開發經驗,希望對各位有用,也希望各位多多支援討論,指出文中不足以及提出您的一些建議。
雙容器
得益於近幾年移動端的發展,前端早已今非昔比,從大型框架來說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了,而是一個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 2 3 4 5 6 7 8 9 10 |
if (shoujibaidu) { //手機百度邏輯 } else if (baiduditu) { //百度地圖邏輯 } else if (nuomi) { //糯米邏輯 } //......其它平臺邏輯 |
這種程式碼十分令人頭疼,所以我們一般會封裝一個方法在底層,哪個平臺有差異就做特殊處理:
1 2 3 4 5 6 7 8 9 |
hybridCallback({ //預設回撥 callback: function() { }, //手機百度回撥 shoubaicallback: function () { }, //...... }); |
這個方法就是用於處理Hybrid差異而生,只有處於某一個環境,才會執行其中的回撥,這其實只是一個語法糖,將判斷的邏輯封裝了,所以這個方案依舊很爛,如果哪天你要多一個容器或者少一個容器,整個站點的程式碼要如何處理呢?如果程式碼量超過萬行,這個程式碼可不好處理!
更好的解決方案是抽離共性,是繼承,一般來說,Hybrid還是有一個很大的特點:主要邏輯與H5一致,一些差異往往是顯示什麼,不顯示什麼(比如糯米中不顯示H5推薦下載APP的廣告),更多的是一些點選回撥的響應,於是我們找到了更好的方案:
多容器解決方案
容器判斷
解決多容器的第一步是容器判斷,一般來說,不同的Webview容器會有不同的userAgent:
1 2 3 4 5 |
//微信中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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
define([ 'AbstractView', 'text!IndexPath/tpl.layout.html' ], function ( AbstractView, layoutHtml ) { return _.inherit(AbstractView, { propertys: function ($super) { $super(); this.template = layoutHtml; this.events = { 'click .js_clickme': 'clickAction' }; }, clickAction: function () { this.showMessage('顯示訊息'); }, initHeader: function (name) { var title = '多Webview容器'; this.header.set({ view: this, title: title, back: function () { console.log('回退'); } }); } }); }); |
1 2 3 4 5 6 7 8 9 10 11 |
propertys: function ($super) { $super(); this.template = layoutHtml; this.events = { 'click .js_clickme': 'clickAction' }; }, clickAction: function () { this.showMessage('顯示訊息'); }, |
首先我們看看這個回撥,假如我們需要做到在糯米容器中使用Native的彈出提示的話,程式碼便有所不同了:
我們使用的應該是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/** * 使用BNJS之前,必須宣告如下BNJSReady函式,確保BNJS相關屬性資訊及頁面載入準備就緒 * BNJSReady直接複製使用,請勿改動 */ var BNJSReady = function (readyCallback) { if(readyCallback && typeof readyCallback == 'function'){ if(window.BNJS && typeof window.BNJS == 'object' && BNJS._isAllReady){ readyCallback(); }else{ document.addEventListener('BNJSReady', function() { readyCallback(); }, false) } } }; BNJSReady(function(){ // 顯示確定和取消按鈕 BNJS.ui.dialog.show({ title: '測試Dialog', message: '我是測試Dialog~~~~', ok: '確定', cancel: '取消', onConfirm: function() { BNJS.ui.toast.show('您剛剛點選了確定按鈕'); }, onCancel: function() { BNJS.ui.toast.show('您剛剛點選了取消按鈕'); } }); // 僅顯示'ok'按鈕 BNJS.ui.dialog.show({ title: '測試Dialog', message: '我是測試Dialog~~~~', ok: 'ok', onConfirm: function() { BNJS.ui.toast.show('您剛剛點選了ok按鈕'); } }); }); |
1 2 3 4 5 6 7 8 9 |
// 僅顯示'ok'按鈕 BNJS.ui.dialog.show({ title: '測試Dialog', message: '我是測試Dialog~~~~', ok: 'ok', onConfirm: function() { BNJS.ui.toast.show('您剛剛點選了ok按鈕'); } }); |
於是我們在index目錄中新增了一個nuomi.index.js的檔案,繼承自index.js,並且在入口檔案main_webviews(原main.js檔案)中做更改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
define([ 'IndexPath/index' ], function ( IndexView ) { return _.inherit(IndexView, { clickAction: function () { BNJS.ui.dialog.show({ title: '測試Dialog', message: '我是測試Dialog~~~~', ok: 'ok', onConfirm: function () { BNJS.ui.toast.show('您剛剛點選了ok按鈕'); } }); } }); }); |
如此,在一般瀏覽器中點選按鈕便是H5的UI元件,在糯米中便是使用的糯米元件了,如果哪天不需要糯米這個平臺將nuomi.js刪除即可:
可以看到,按鈕的點選已經不一樣了,當然還有很多不足,比如糯米中header部分便沒有做處理。
header元件
header這種元件與上述問題又不一致,這種不一致主要體現在兩個方面:
① 由於底層實現問題,做不到一致
比如手機百度就不支援返回按鈕定製,就連最簡單的title改變都是直接監聽的document.title的變化,並且Andriod還有BUG,像這種底層實現直接就抹殺的基本沒法,一般來說就是把原來的header換個方式顯示在頁面中,可以是弧形按鈕,可以是其它方式。
② header是系統級別的操作,不應該由使用者控制
如同該文中對header元件的處理:淺談Hybrid技術的設計與實現,像header這一類元件,這類元件必須滿足在H5站點與Hybrid中API使用一致,而底層實現各異,與之前不同的是,這裡的header元件要考慮的可不止2個平臺那種問題了,他可能是這樣的:
1 2 3 |
ui.eader //H5站點使用 nuomi.ui.header //糯米使用 xx.ui.header //...... |
我們這裡將場景變小,暫時只考慮糯米與H5的實現,於是會在底層多出一個header的實現:
我這裡工作做的多一些,考慮了微信時候的場景,但是這裡業務程式碼暫時只考慮糯米,對應糯米的文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
define([], function () { 'use strict'; return _.inherit({ propertys: function () { }, //全部更新 set: function (opts) { if (!opts) return; var i, len, item; var scope = opts.view || this; //處理返回邏輯 if (opts.back && typeof opts.back == 'function') { BNJS.page.onBtnBackClick({ callback: $.proxy(opts.back, scope) }); } else { BNJS.page.onBtnBackClick({ callback: function () { if (history.length > 0) history.back(); else BNJS.page.back(); } }); } //處理title if (typeof opts.title == 'string') { BNJS.ui.title.setTitle(opts.title); } //刪除右上角所有按鈕【1.3】 //每次都會清理右邊所有的按鈕 BNJS.ui.title.removeBtnAll(); //處理右邊按鈕 if (typeof opts.right == 'object' && opts.right.length) { for (i = 0, len = opts.right.length; i < len; i++) { item = opts.right[i]; BNJS.ui.title.addActionButton({ tag: _.uniqueId(), text: item.value, callback: $.proxy(item.callback, scope) }); } } }, show: function () { }, hide: function () { }, //只更新title update: function (title) { }, initialize: function () { //隱藏H5頭 $('#headerview').hide(); this.propertys(); } }); }); |
程式碼實現很簡單,只要保持與H5使用API一致即可,這個時候再簡單改下入口檔案,便能適配了。
PS:注意,這裡的適配只是簡單實現,考慮多場景的話不能這樣寫程式碼!!!
於是我們在糯米中便能很好的執行了
結語
程式碼地址
https://github.com/yexiaochai/mvc
demo地址
http://yexiaochai.github.io/mvc/webapp/bus/index.html
測試糯米時請掃描第二個二維碼:
這裡丟擲了前端多Webview容器會遇到的一些問題,並提出了一個解決思路,後續可能會有更加完整解決方案與demo出來,希望對各位有用,若是有已經涉及到這塊業務的朋友可以私下交流下。