background

According to the requirements of our business side, we need to develop an APP. However, due to time constraints, the shell APP can only be adopted, that is, the native APP has webView embedded to display the front-end page. This article mainly describes the communication between JavaScript and native APP, as well as embedded WebView development, some front-end aspects may step on the pit.

The technical architecture

Front end: VUE + VUex + VUE-Router + Webpack Family bucket development back end: Node(Express framework) simple forwarding interface to Java – true back end interface.

Js communicates with native

JsBridge technology and native APP are used to communicate with Android portal and ios portal. Because the two platforms have different initialization methods, corresponding operations need to be done for each platform in the development process. The specific practices

  1. Declare initialization functions as required by the library
//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)
	})
})
Copy the code
  1. Initialize to get the bride object. Native APP defined methods can be called or js methods registered for native invocation
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)
	})
})
Copy the code

Tips:

  • The initial configuration of Android and IOS is different, and you need to determine the platform before invoking. In addition, some additional methods need to be introduced when Android is initialized.
  • When the Android definition method is called, the return value can only be a string. IOS can be a JSON object. You need to encapsulate the returned value or specify a uniform data format during callHandler.
  • The complete business code is given at the end

Hit the pit

  1. Call bridge attribute method registerHandler callHandler, when handling page logic within the callback function, had better avoid to use this
  2. Under the VUE component, when the vUE instance is used within the registerHandler, callHandler callback function, the instance object cannot be obtained. The correct way is to use the vue instance object by calling the window object method inside the callback function.
/ / the vue components
mounted(){
    window['handleServicePushMessage'] = (res) = > {
    	vm.handleServicePushMessage(res)
    };
    bridge.registerHandler("servicePushMessage".function (data, responseCallback) {
        handleServicePushMessage(data)
        responseCallback(data) // Can pass the value to App})}Copy the code
  1. When the desktop push message is clicked to jump to the details in the App, the JS registration method may be invoked repeatedly. Therefore, the method needs to make repeated call judgment
  2. In ios-12.0 version, in the page with input box, the soft keyboard will jack up the WebView when typing, when losing focus, the WebView will not automatically rebound. Need to call APP to do processing pull back interface.
// Resolve the problem that IOS 12 UI does not automatically pull back
document.addEventListener('focusout'.function (event) {
	let curTarget = event.target || event.srcElement;
	let isInput= ['input'.'textarea'];
	// Handle the case where the page is clicked continuously as an input box
	let curTargetTagName= curTarget.tagName.toLowerCase();
	    if (isInput.includes(curTargetTagName)) {
    	    // Event processing
    	    // Delay obtaining the activeElement before judging
        	setTimeout(function () {
        	    let activeEle = document.activeElement;
        	    let activeEleTagName= activeEle.tagName.toLowerCase();
        	    if(! isInput.includes(activeEleTagName)) {// console.log(document.activeElement.tagName);
        	        // Call the app bridge to pull back the WebView
        	        performMethod('scrollTotop'.null); }},200); }},true);
Copy the code

5. When JS calls a bridge that does not exist in app, exceptions cannot be caught and the page will not report errors 6. The navigation bar shows a problem. Because the project time is tight and app developers do not have too many development tasks, routing control is put in the front end. At this point, there is a navigation bar battery time bar adaptation problem. In this project, the top is lowered 20PX, and the font color control of the battery time bar is also set through the bridge call. In addition, the iPhone X is suitable for other processing. 7. When the APP finishes loading the webpage and JS immediately calls the native method bridge, it may appear that the native method bridge has not been registered. Therefore, special cases need to delay the invocation of the bridge operation.

The complete code

/* Determine the platform */
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_]+))? /);
	variphone = ! 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)
/* Introduce the initialization required by Android, IOS does not perform, such as performing the IOS end bridge call will be affected */
(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, trigger native processing 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;
	}

	This function returns sendMessageQueue to Native. Since Android can't get the returned content directly, shouldOverrideUrlLoading is used to return the content
	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);
	}

	/* For native use,*/
	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 {/* Send */ directly
				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];
				}
				/* Find the specified handler*/
				try {
					handler(message.data, responseCallback);
				} catch (exception) {
					if (typeof console! ='undefined') {
						console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); }}}}); }ReceiveMessageQueue is assigned to null after the page is loaded, so */
	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 initialization function */
function connectWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) {
		callback(WebViewJavascriptBridge)
	} else {
		document.addEventListener('WebViewJavascriptBridgeReady'.function () {
			callback(WebViewJavascriptBridge)
		}, false); }}/*IOS initialization function */
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){
        /* Mount the global object */
        window.BRIDGE= brige; })}if(device.android){
    connectWebViewJavascriptBridge(function(bridge){
        /* Mount the global object */
        window.BRIDGE= brige; })}Copy the code