zoukankan      html  css  js  c++  java
  • H5在WebView上开发小结

    背景

    来自我司业务方要求,需开发一款APP。但由于时间限制,只能采取套壳app方式,即原生app内嵌webview展示前端页面。本文主要记述JavaScript与原生app间通信,以及内嵌webview开发时,前端方面可能踩的一些坑。

    技术架构

    前端:vue+vuex+vue-router+webpack全家桶开发
    后端:Node(express框架)简单转发接口至java-真后端接口。

    js与原生通信

    采用jsBridge技术和原生APP通信
    android 传送门 和ios 传送门,因为两个平台初始化方式不同,因此在开发过程中,需针对每个平台做对应操作。 具体做法

    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:

    • 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立即调用原生方法桥时,可能出现原生方法桥未注册完情况。故特殊情况需延迟调用桥操作。

    完整代码

    /*判断平台*/
    function (window) {
    	window.device = {};
    	var ua = navigator.userAgent;
    	var android = ua.match(/(Android);?[s/]+([d.]+)?/);
    	var ipad = ua.match(/(iPad).*OSs([d_]+)/);
    	var ipod = ua.match(/(iPod)(.*OSs([d_]+))?/);
    	var iphone = !ipad && ua.match(/(iPhonesOS)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;
        })
    }
    
    复制代码
    关注下面的标签,发现更多相似文章
  • 相关阅读:
    mybatis入参错误:There is no getter for property named ‘status‘ in ‘class java.lang.Integer‘
    JAVA程序员面试笔试题(一)
    Java8新特性LocalDateTime获取周几
    linux常用命令记录 screen
    ubuntu 19.04 + lenovo-xiaoxin-I2000 触摸板右键单击无法使用
    华为交换路由常用命令
    centos7常用软件
    一般网络延迟高的原因
    华为防火墙进程&简单配置
    私网互联(本质是三层路由)
  • 原文地址:https://www.cnblogs.com/zhangycun/p/10450619.html
Copyright © 2011-2022 走看看