H5在WebView上開發小結

背景

來自我司業務方要求,需開發一款APP。但因爲時間限制,只能採起套殼app方式,即原生app內嵌webview展現前端頁面。本文主要記述JavaScript與原生app間通訊,以及內嵌webview開發時,前端方面可能踩的一些坑。javascript

技術架構

前端:vue+vuex+vue-router+webpack全家桶開發
後端:Node(express框架)簡單轉發接口至java-真後端接口。前端

js與原生通訊

採用jsBridge技術和原生APP通訊
android 傳送門 和ios 傳送門,由於兩個平臺初始化方式不一樣,所以在開發過程當中,需針對每一個平臺作對應操做。 具體作法vue

  1. 按照庫要求,聲明好初始化函數
//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)
	})
})
複製代碼
  1. 初始化,獲得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時,對返回值進行封裝處理或統一規定好數據格式。
  • 完整業務代碼文末給出

踩坑

  1. 調用bridge屬性方法registerHandler,callHandler,在回調函數內處理頁面邏輯時,最好避免使用this
  2. 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
    })
}
複製代碼
  1. 桌面推送消息點擊跳轉至App內詳情狀況下,js註冊方法供調用時,可能會引發重複調用的問題。故在方法內需作好重複調用判斷
  2. 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;
    })
}

複製代碼
相關文章
相關標籤/搜索