Native(以Android為例)和H5通訊,基本原理:
- Android調(diào)用H5:通過webview類的loadUrl方法可以直接執(zhí)行js代碼型宙,類似瀏覽器地址欄輸入一段js一樣的效果
String javascriptCommand = String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');", messageJson);
webview.loadUrl(javascriptCommand);
- H5調(diào)用Android:webview可以攔截H5發(fā)起的任意url請求均芽,webview通過約定的規(guī)則對攔截到的url進行處理(消費)贬派,即可實現(xiàn)H5調(diào)用Android
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'ddjsscheme://***';
document.documentElement.appendChild(WVJBIframe);
實現(xiàn)步驟
1. H5調(diào)用原生方法實現(xiàn)步驟
h5調(diào)用原生方法有4個關(guān)鍵點:
1. 通訊媒介——原生通訊協(xié)議
ddjsscheme://return/_fetchQueue/[{"handlerName":"dd.native.call","data":{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}},"callbackId":"cb_1_1504142561851"}]
通過webview攔截到自定義約定的協(xié)議俐芯,就可以做相應(yīng)的處理篮绰。
2. 通訊行為——觸發(fā)
能被原生監(jiān)聽并捕獲截攔的H5行為肄渗,都可以作為原生通訊協(xié)議的觸發(fā)行為镇眷。Hybrid H5 的這類行為有 console.log、alert翎嫡、confirm欠动、prompt、location.href 等。將原生協(xié)議內(nèi)容通過其中的某一行為觸發(fā)具伍,即可被原生正確捕獲并解析翅雏。本文通過:WVJBIframe.src = 'ddjsscheme://';
觸發(fā)。
觸發(fā)詳細流程
當h5頁面調(diào)用如下接口時
dd.biz.navigation.setTitle({
title: '郵箱正文',
onSuccess: function(data) {},
onFail: function(error) {}
})
接著會調(diào)用此方法
function _executeFuction (methodName, options) {
//alert('_executeFuction' + methodName)
if (dd.isInit) {
!!window['_' + methodName] && window['_' + methodName](options)
} else {
!!dd && setupWebViewJavascriptBridge(function () {
if (window.WebViewJavascriptBridge.init) {
if (!dd.isInit) {
//alert('WebViewJavascriptBridge init')
window.WebViewJavascriptBridge.init(function(message, responseCallback) {})
}
}
dd.isInit = true;
setTimeout(function() {
!!window['_' + methodName] && window['_' + methodName](options)
}, 100);
});
}
}
以本人的半杯水js水平來看人芽,先調(diào)用setupWebViewJavascriptBridge
方法:
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'ddjsscheme://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
如果WebViewJavascriptBridge加載完成就調(diào)用window.WebViewJavascriptBridge.init(function(message, responseCallback) {})
;如果沒有加載完成WVJBIframe.src = 'ddjsscheme://__bridge_loaded__';
然后native攔截加載本地的WebViewJavascriptBridge
if (url.equals("ddjsscheme://__bridge_loaded__")) {
//加載bridge
BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs);
}
接著回來說window.WebViewJavascriptBridge.init(function(message, responseCallback) {})
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]);
}
}
//提供給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 {
//直接發(fā)送
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.data, message.data, exception);
}
}
}
});
}
//sendMessage add message, 觸發(fā)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 ='ddjsscheme://__WVJB_QUEUE_MESSAGE__';
}
js不太懂望几,能看懂流程就可以啦,特別注意_doSend
方法里的這行sendMessageQueue.push(message);
代碼萤厅,message就是就是我們從h5調(diào)用接口傳過來的參數(shù)橄抹,后面會用到!這時又發(fā)出一條協(xié)議messagingIframe.src ='ddjsscheme://__WVJB_QUEUE_MESSAGE__';
,我們再去本地看看做了什么操作:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //攔截'ddjsscheme://__WVJB_QUEUE_MESSAGE__'
webView.flushMessageQueue();
return true;
} else {
return shouldOverrideUrl(view, url);
}
}
void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// deserializeMessage
List<Message> list = null;
try {
list = Message.toArrayList(data);
} catch (Exception e) {
e.printStackTrace();
return;
}
if (list == null || list.size() == 0) {
return;
}
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 是否是response
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);
responseCallbacks.remove(responseId);
} else {
CallBackFunction responseFunction = null;
// if had callbackId
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
}
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null){
handler.handle(m.getData(), responseFunction);
}
}
}
}
});
}
}
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback)
}
這里的回調(diào)暫時先不關(guān)注惕味,后面我們在分析楼誓,注意responseCallbacks
這個是個 map集合。它的value就是一個回調(diào)函數(shù)赦拘,同時還需知道responseFunction
這個是native返回給h5的回調(diào)慌随,這里只需知道有這么個概念,后面我們會講解躺同。this.loadUrl(jsUrl);
這里又加載了一段jsjavascript:WebViewJavascriptBridge._fetchQueue();
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);
}
這是重點阁猜! 這是重點! 這是重點蹋艺!重要的事說三遍剃袍!這里把sendMessageQueue
轉(zhuǎn)換成json格式方便native解析,然后把sendMessageQueue
回收掉捎谨,又到了我們熟悉的
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
再看一下本地做了什么操作:
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回數(shù)據(jù)
webView.handlerReturnData(url);
return true;
} else {
return shouldOverrideUrl(view, url);
}
}
void handlerReturnData(String url) {
String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = BridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
return;
}
}
這里f.onCallBack(data);
調(diào)用的就是flushMessageQueue()
里的回調(diào)民效,data
就是h5傳過來的參數(shù)
[{"handlerName":"dd.native.call","data":{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}},"callbackId":"cb_2_1504156072331"}]
第一個handlerName
是作為messageHandlers
map的鍵;
第二個handlerName
是native方法路徑
params
是h5傳過來的設(shè)置標題的值
callbackId
用來回調(diào)的在回調(diào)這節(jié)再去講解
至此涛救,h5與native通信的觸發(fā)過程講解完畢畏邢,下節(jié)我們一起看看flushMessageQueue()
里的回調(diào)是如何做處理的。
3. 通訊行為——處理
在flushMessageQueue()
里有這么一段代碼
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
}
if (handler != null) {
handler.handle(m.getData(), responseFunction);
}
這個handler意義在于處理h5傳過來的數(shù)據(jù)检吆,使用之前需要注冊:
public void registerHandler(String handlerName, BridgeHandler handler) {
if (handler != null) {
messageHandlers.put(handlerName, handler);
}
}
webView.registerHandler(sFunction, new BridgeHandler() {
@Override
public void handle(String data, CallBackFunction function) {
dispatchTask(data, function);
}
});
注冊時需要鍵值對舒萎,鍵就是之前說的第一個handlerName
,value是BridgeHandler
,調(diào)用handle(String data, CallBackFunction function)
時傳入就data是:
{"handlerName":"biz.navigation.setTitle","params":{"title":"標題"}}
function
就是用于回調(diào)給h5的,下面再看一下dispatchTask(data, function);
private void dispatchTask(String data, CallBackFunction callBackFunction) {
try {
JSONObject jo = new JSONObject(data);
final String handlerName = jo.getString("handlerName");
String params = jo.getString("params");
Log.d(TAG, "# ============ ");
Log.d(TAG, "# request call ---> " + handlerName);
Log.d(TAG, "# request params ---> " + params);
// 接口調(diào)用每次都只執(zhí)行一次,為了能多次執(zhí)行,這里需要再重復(fù)調(diào)用原來的接口
if (null == callBackFunction) {
callBackFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
mWebView.callHandler(handlerName, data, null);
}
};
}
String[] split = handlerName.split("\\.");
String packageName = "***";
String className = split[split.length - 2];
String methodName = split[split.length - 1];
for (int i = 0; i < split.length - 2; i++) {
packageName += "." + split[i];
}
packageName +=
"." + Character.toUpperCase(className.charAt(0)) + className.substring(1);
Class<?> clazz = Class.forName(packageName);
Constructor constructor = clazz.getConstructor(BridgeWebView.class, Context.class);
Object obj = constructor.newInstance(mWebView, this);
Method method = clazz.getMethod(methodName, String.class, CallBackFunction.class);
method.invoke(obj, params, callBackFunction);
} catch (JSONException | IllegalAccessException | InvocationTargetException
| NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
e.printStackTrace();
}
}
知道了方法路徑蹭沛,參數(shù)利用反射一切都水到渠成了臂寝,再看一下setTitle的處理:
public void setTitle(String data, CallBackFunction callBackFunction) {
String result = null;
JSONObject jo = new JSONObject();
try {
Log.d("setTitle==TTT===", data);
JSONObject params = new JSONObject(data);
String title = params.optString("title", "");
if (mContext != null) {
((HybridActivity) mContext).setHeadTitle(title);
}
jo.put("result", true);
result = new ResultCallBack().onSuccessResult(jo);
} catch (JSONException e) {
e.printStackTrace();
try {
jo.put("result", false);
} catch (JSONException e1) {
e1.printStackTrace();
}
result = new ResultCallBack()
.onFailResult(jo);
}
callBackFunction.onCallBack(result);
}
這樣h5調(diào)用native的處理步驟講解完畢,下面通過CallBackFunction講解如何回調(diào)給js的摊灭。
4. 通訊行為——回調(diào)
還記得在觸發(fā)那個步驟中的flushMessageQueue
的這一段代碼嗎:
CallBackFunction responseFunction = null;
// if had callbackId
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
}
};
}
當調(diào)用這段callBackFunction.onCallBack(result);
代碼咆贬,就會回調(diào)到上面那段代碼,在queueMessage(responseMsg);
又會調(diào)用
String javascriptCommand = String.format("javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');", messageJson);
webview.loadUrl(javascriptCommand);
js會把參數(shù)回調(diào)給h5帚呼,這樣h5調(diào)用native的流程就走通了掏缎。
2. 原生調(diào)用H5方法實現(xiàn)步驟
未完待續(xù)