来自我司业务方要求,需开发一款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; }) } 复制代码