來自我司業務方要求,需開發一款APP。但因爲時間限制,只能採起套殼app方式,即原生app內嵌webview展現前端頁面。本文主要記述JavaScript與原生app間通訊,以及內嵌webview開發時,前端方面可能踩的一些坑。javascript
技術架構
前端:vue+vuex+vue-router+webpack全家桶開發
後端:Node(express框架)簡單轉發接口至java-真後端接口。前端
js與原生通訊
採用jsBridge技術和原生APP通訊
android 傳送門 和ios 傳送門,由於兩個平臺初始化方式不一樣,所以在開發過程當中,需針對每一個平臺作對應操做。 具體作法vue
- 按照庫要求,聲明好初始化函數
//android function connectWebViewJavascriptBridge{ if (window.WebViewJavascriptBridge) { //do your work here } else { document.addEventListener( 'WebViewJavascriptBridgeReady' , function() { //do your work here }, false ); } } //ios setupWebViewJavascriptBridge(function(bridge) { /* Initialize your app here */ bridge.registerHandler('JS Echo', function(data, responseCallback) { console.log("JS Echo called with:", data) responseCallback(data) }) bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) { console.log("JS received response:", responseData) }) }) 複製代碼
- 初始化,獲得bride對象。則可調用原生app已定義方法或註冊js方法供原生調用
setupWebViewJavascriptBridge(function(bridge) { /* Initialize your app here */ bridge.registerHandler('JS Echo', function(data, responseCallback) { console.log("JS Echo called with:", data) responseCallback(data) // }) bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) { console.log("JS received response:", responseData) }) }) 複製代碼
Tips:java
- Android 與 IOS初化方式不一樣,須要判斷平臺後再進行調用。另外Android初始化時,需額外引入一些方法。
- 調用Android定義方法時,返回值只能爲字符串。而IOS可爲JSON對象。須要在callHandler時,對返回值進行封裝處理或統一規定好數據格式。
- 完整業務代碼文末給出
踩坑
- 調用bridge屬性方法registerHandler,callHandler,在回調函數內處理頁面邏輯時,最好避免使用this
- vue組件下,在registerHandler,callHandler回調函數內使用vue實例時,沒法獲取實例對象。正確作法是在回調函數內調用window對象下方法,再經過該方法去使用vue實例對象。
//vue 組件 mounted(){ window['handleServicePushMessage'] = (res) => { vm.handleServicePushMessage(res) }; bridge.registerHandler("servicePushMessage", function (data, responseCallback) { handleServicePushMessage(data) responseCallback(data) //可傳值到App }) } 複製代碼
- 桌面推送消息點擊跳轉至App內詳情狀況下,js註冊方法供調用時,可能會引發重複調用的問題。故在方法內需作好重複調用判斷
- IOS-12.0版本下,在有輸入框的頁面,輸入時軟鍵盤會頂起webview,當失去焦點時,webview不會自動回彈。需調用APP作處理拉回界面。
//解決ios 12版本 ui不自動回拉問題 document.addEventListener('focusout', function (event) { let curTarget = event.target || event.srcElement; let isInput= ['input', 'textarea']; //處理頁面連續點擊都爲輸入框的狀況 let curTargetTagName= curTarget.tagName.toLowerCase(); if (isInput.includes(curTargetTagName)) { //事件處理 //延遲獲取activeElement再進行判斷 setTimeout(function () { let activeEle = document.activeElement; let activeEleTagName= activeEle.tagName.toLowerCase(); if (!isInput.includes(activeEleTagName)) { // console.log(document.activeElement.tagName); //調用app橋拉回webview performMethod('scrollTotop', null); } }, 200); } }, true); 複製代碼
5.當js調用app不存在的橋時,沒法捕獲異常,頁面不會報錯
6.導航欄顯示問題,因爲項目時間緊迫,而且app開發人員不承載太多開發任務,因此路由控制放在前端處理。此時就有導航欄電池時間欄的適配問題。本項目採用頂部下調20PX處理,電池時間欄字體顏色的控制也是經過橋調用來設置;另外iPhone X適配另外處理。
7.當app加載完網頁時,js當即調用原生方法橋時,可能出現原生方法橋未註冊完狀況。故特殊狀況需延遲調用橋操做。android
完整代碼
/*判斷平臺*/ function (window) { window.device = {}; var ua = navigator.userAgent; var android = ua.match(/(Android);?[\s\/]+([\d.]+)?/); var ipad = ua.match(/(iPad).*OS\s([\d_]+)/); var ipod = ua.match(/(iPod)(.*OS\s([\d_]+))?/); var iphone = !ipad && ua.match(/(iPhone\sOS)\s([\d_]+)/); device.ios = device.android = device.iphone = device.ipad = device.androidChrome = false; if (android) { device.os = 'android'; device.osVersion = android[2]; device.android = true; device.androidChrome = ua.toLowerCase().indexOf('chrome') >= 0 } if (ipad || iphone || ipod) { device.os = 'ios'; device.ios = true } }(window) /*引入Android須要的初始化,IOS不執行,如執行IOS端橋調用會受影響*/ (function () { if (window.WebViewJavascriptBridge || device.ios) { return false; } var messagingIframe; var sendMessageQueue = []; var receiveMessageQueue = []; var messageHandlers = {}; var CUSTOM_PROTOCOL_SCHEME = 'yy'; var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; var responseCallbacks = {}; var uniqueId = 1; function _createQueueReadyIframe(doc) { messagingIframe = doc.createElement('iframe'); messagingIframe.style.display = 'none'; doc.documentElement.appendChild(messagingIframe); } /*set default messageHandler*/ function init(messageHandler) { if (WebViewJavascriptBridge._messageHandler) { throw new Error('WebViewJavascriptBridge.init called twice'); } WebViewJavascriptBridge._messageHandler = messageHandler; var receivedMessages = receiveMessageQueue; receiveMessageQueue = null; for (var i = 0; i < receivedMessages.length; i++) { _dispatchMessageFromNative(receivedMessages[i]); } } function send(data, responseCallback) { _doSend({data: data}, responseCallback); } function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } function callHandler(handlerName, data, responseCallback) { _doSend({handlerName: handlerName, data: data}, responseCallback); } /*sendMessage add message, 觸發native處理 sendMessage*/ function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; } /* 提供給native調用,該函數做用:獲取sendMessageQueue返回給native,因爲android不能直接獲取返回的內容,因此使用url shouldOverrideUrlLoading 的方式返回內容*/ function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; /*android can't read directly the return data, so we can reload iframe src to communicate with java*/ messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString); } /*提供給native使用,*/ function _dispatchMessageFromNative(messageJSON) { setTimeout(function () { var message = JSON.parse(messageJSON); var responseCallback; /*java call finished, now need to call js callback function*/ if (message.responseId) { responseCallback = responseCallbacks[message.responseId]; if (!responseCallback) { return; } responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else {/*直接發送*/ if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function (responseData) { _doSend({responseId: callbackResponseId, responseData: responseData}); }; } var handler = WebViewJavascriptBridge._messageHandler; if (message.handlerName) { handler = messageHandlers[message.handlerName]; } /*查找指定handler*/ try { handler(message.data, responseCallback); } catch (exception) { if (typeof console != 'undefined') { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); } } } }); } /*提供給native調用,receiveMessageQueue 在會在頁面加載完後賦值爲null,因此*/ function _handleMessageFromNative(messageJSON) { if (receiveMessageQueue && receiveMessageQueue.length > 0) { receiveMessageQueue.push(messageJSON); } else { _dispatchMessageFromNative(messageJSON); } } var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, send: send, registerHandler: registerHandler, callHandler: callHandler, _fetchQueue: _fetchQueue, _handleMessageFromNative: _handleMessageFromNative }; var doc = document; _createQueueReadyIframe(doc); var readyEvent = doc.createEvent('Events'); readyEvent.initEvent('WebViewJavascriptBridgeReady'); readyEvent.bridge = WebViewJavascriptBridge; doc.dispatchEvent(readyEvent); })(); /*Android端初始化函數*/ function connectWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { callback(WebViewJavascriptBridge) } else { document.addEventListener('WebViewJavascriptBridgeReady', function () { callback(WebViewJavascriptBridge) }, false); } } /*IOS端初始化函數*/ function setupWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge) } else { } if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback) } window.WVJBCallbacks = [callback]; var WVJBIframe = document.createElement('iframe'); WVJBIframe.style.display = 'none'; WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; document.documentElement.appendChild(WVJBIframe); setTimeout(function () { document.documentElement.removeChild(WVJBIframe) }, 0) } if(device.ios){ setupWebViewJavascriptBridge(function(bridge){ /*掛載上全局對象*/ window.BRIDGE= brige; }) } if(device.android){ connectWebViewJavascriptBridge(function(bridge){ /*掛載上全局對象*/ window.BRIDGE= brige; }) } 複製代碼