導(dǎo)語
本文將講解 UIWebView 和 WKWebView 通過 WebViewJavascriptBridge 三方庫進(jìn)行通信的原理;梳理OC -- > JS凤粗, JS --> OC 調(diào)動交互的流程辅柴;和OC方薯鳍、JS方各自做了具體什么操作才使Native與H5有了交互的功能沉衣。本文并不會去講解 WebViewJavascriptBridge 庫的使用和方法的講解盔然,你可以通過以下參考內(nèi)容去了解愉烙。
- 使用WKWebView替換UIWebView
http://www.reibang.com/p/6ba2507445e4 - WebViewJavascriptBridge淺析
http://www.cnblogs.com/LeeGof/p/8143408.html - WKWebView 那些坑
https://mp.weixin.qq.com/s/rhYKLIbXOsUJC_n6dt9UfA
OC 調(diào)用 JS
OC 相關(guān)代碼: 調(diào)用與JS商量好的 oc_to_js_js’registerFunctionName 函數(shù)
[_webViewBridge callHandler:@“oc_to_js_js’registerFunctionName” data:@"oc傳給js的數(shù)據(jù)” responseCallback:^(id responseData) {
NSLog(@"JS執(zhí)行完回調(diào)給OC的數(shù)據(jù):%@“,responseData);
}];
JS 相關(guān)代碼: 注冊一個叫 oc_to_js_js’registerFunctionName 的函數(shù)
setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('oc_to_js_js’registerFunctionName', function(data, responseCallback) {
alert(‘oc成功調(diào)用js讨盒,并打印數(shù)據(jù)data:’+data);
responseCallback(‘js執(zhí)行完回調(diào)給OC的數(shù)據(jù)');
})
})
如上OC和JS代碼可以看出,OC需要調(diào)用JS注冊的‘ oc_to_js_js’registerFunctionName’ 方法步责,并傳遞數(shù)據(jù)data給JS返顺;JS在收到OC的消息后alert了傳遞過來的數(shù)據(jù),并回傳了一份數(shù)據(jù)給OC蔓肯。
那么這個 oc--data--js--data--oc 的流程到底是怎樣的呢
大致步驟:
- 步驟一 (oc環(huán)境) : OC先聲明一段JS代碼遂鹊,在第一次進(jìn)入此webView頁的時候注入到 webView中,之后webView上下文中就有了這些js函數(shù)蔗包。
注入JS代碼
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
...
if ([_base isQueueMessageURL:navigationAction.request.URL]) {
[webView stringByEvaluatingJavaScriptFromString:@“js代碼”]
}
...
}
- 步驟二 (js環(huán)境) : 注冊聲明 js 會被 oc 調(diào)用的方法存放在 js 環(huán)境的字典中秉扑;
保存js注冊的函數(shù)
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
- 步驟三 (oc環(huán)境) :oc 發(fā)起調(diào)用,生成一個message字典调限,三個參數(shù)(handlerName 方法名舟陆,data數(shù)據(jù), callbackId 回調(diào)方法)
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
- 步驟四 (oc環(huán)境) : 把message字典轉(zhuǎn)成json字符串旧噪,處理字符串里面的\吨娜、\、\r淘钟、\n 等特殊字符宦赠。生成新的字符串并加上
WebViewJavascriptBridge._handleMessageFromObjC(‘messageJSON‘)陪毡,形成一段字符串形式的js代碼(既通過提前注入js中的函數(shù)_handleMessageFromObjC去處理 messageJSON 參數(shù))。
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message pretty:NO];
[self _log:@"SEND" json:messageJSON];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
}
- 步驟五 (oc環(huán)境) : 通過調(diào)用oc系統(tǒng)的 stringByEvaluatingJavaScriptFromString 方法讓webView執(zhí)行上述js代碼勾扭。 注: WKWebView 為 evaluateJavaScript 方法.
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
dispatch_sync(dispatch_get_main_queue(), ^{
[webView stringByEvaluatingJavaScriptFromString:javascriptCommand]
});
- 步驟六 (js環(huán)境) : 通過 oc 傳入過來的json數(shù)據(jù)毡琉,解析后取出 functionName 和 callbackId,從事先注冊好的緩存中獲取對應(yīng)handler函數(shù)和callback函數(shù)妙色, 并執(zhí)行桅滋。
取出 callback 函數(shù)和 handler 函數(shù)執(zhí)行調(diào)用,在handler中執(zhí)行 callback 數(shù)據(jù)回傳操作身辨。
responseCallback = responseCallbacks[message.responseId];
var handler = messageHandlers[message.handlerName];
if (handler) {
handler(message.data, responseCallback);
}
- 步驟七 (js環(huán)境) :在js的 callback 函數(shù)執(zhí)行完后丐谋,需要從js環(huán)境發(fā)生數(shù)據(jù)給oc環(huán)境,這時候js會改變html根節(jié)點下隱性的<iframe>標(biāo)簽的src煌珊,當(dāng)iframe的src改變時會發(fā)起Request号俐,這時候oc就能監(jiān)聽到j(luò)s發(fā)生了Request操作 。 這個環(huán)節(jié)的具體操作也就跟下面即將要講的 js 調(diào)用 oc 的流程類似了定庵。
JS 調(diào)用 OC
JS調(diào)用OC 一般會有三種方式
- iOS7 引入了 JavaScriptCore吏饿,可以初始化一個 JSContext 對象,然后約定好一個方法名就好了蔬浙。
- 特殊的一個 Scheme 猪落。客戶端這邊會攔截到這種指令格式的 URL 需求畴博,實現(xiàn)一個 JS 到 Native 傳遞消息的一個過程
- 輪詢笨忌,對于 JS 他需要把給 Native 傳遞的消息,轉(zhuǎn)化成一個 JSON 绎晃,客戶端這邊一般會開一個線程蜜唾,每隔一段時間會調(diào) JS 的方法,從這個方法里面把 JS 需要給 Native 傳遞的消息全部取出來庶艾,取出來之后再去做相應(yīng)的操作袁余,客戶端開銷比較大。
本文只要講解第二種方式咱揍,通過Scheme去實現(xiàn)一個 JS 到 Native 傳遞消息的一個過程颖榜。
OC 代碼 : 注冊一個叫 js_to_oc_oc’registerFunctionName 的方法
[webViewBridge registerHandler:@"js_to_oc_oc’registerFunctionName” handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@“js成功調(diào)用oc, 并獲取數(shù)據(jù)data”);
responseCallback(@“oc回調(diào)給js的數(shù)據(jù) 如:支付失敗”);
}];
JS 代碼 : 調(diào)用與OC商量好的 js_to_oc_oc’registerFunctionName 方法
WebViewJavascriptBridge.callHandler('js_to_oc_oc’registerFunctionName', "{\"content\" : \"js傳遞給oc的數(shù)據(jù)內(nèi)容\”}”, function(response) {
alert('oc成功被調(diào)用煤裙,并回傳給js的回調(diào)數(shù)據(jù):’ + response);
document.getElementById("returnValue").value = response;
});
如上OC和JS方法可以看出掩完, JS需要調(diào)用OC注冊的‘ js_to_oc_oc’registerFunctionName’ 方法,并傳遞數(shù)據(jù)json字符串給OC硼砰;OC在收到JS的消息后log了傳遞過來的數(shù)據(jù)且蓬,并回傳了一份數(shù)據(jù)給JS。
大致步驟:
步驟一 (oc環(huán)境) : OC先聲明一段 js 通用代碼题翰,在第一次進(jìn)入此webView頁的時候注入到 webView中恶阴,之后webView上下文中就有了這些js方法(如果已注入過忽略此步)诈胜。
步驟二 (oc環(huán)境) : 注冊oc方法, 存儲一個message字典冯事,以 handlerName 為key焦匈, handler 為value 。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
- 步驟三 (js環(huán)境) : js 發(fā)起調(diào)用方法昵仅,傳入handlerName缓熟,data,callBack 參數(shù)摔笤, 生成message字典够滑,保存在js環(huán)境中。
function callHandler(handlerName, data, responseCallback) {
...
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
sendMessageQueue.push(message);
}
- 步驟四 (js環(huán)境) :js發(fā)起調(diào)用時籍茧,會對當(dāng)前html中的<iframe> 標(biāo)簽的src重新賦值一個scheme版述, <iframe> 標(biāo)簽會在src改變時自動發(fā)起Request跳轉(zhuǎn), 因此 oc 才會收到消息(下一步)
最開始注入的js代碼中,會對當(dāng)前html添加一個<iframe>, 并設(shè)置為隱性
iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(iframe);
重新賦值iframe的src
iframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
- 步驟五 (oc環(huán)境) :WKWebview 和 UIWebview 都會有個 decidePolicyForNavigationAction 的代理方法可以攔截Request的url寞冯, 對url的scheme進(jìn)行判斷是普通跳轉(zhuǎn)還是JS跳轉(zhuǎn)。
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
...
if ([_base isCorrectProcotocolScheme:url]) {
...
// js 跳轉(zhuǎn)晚伙,攔截處理
decisionHandler(WKNavigationActionPolicyCancel);
}else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
- 步驟六 (oc環(huán)境) :如果是js跳轉(zhuǎn)吮龄, 通過系統(tǒng)的 stringByEvaluatingJavaScriptFromString 方法調(diào)用 ‘WebViewJavascriptBridge._fetchQueue()’ 這段注入過的js代碼, 獲取此方法的返回值咆疗, 既獲取步驟3的 message漓帚。 注: WKWebView 為 evaluateJavaScript 方法獲取.
[webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
// result 為獲取到的函數(shù)信息(以 handlerName、data午磁、callback) 為鍵的json字符串
}];
js 函數(shù)
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
- 步驟七 (oc環(huán)境) :根據(jù)從js獲取到的函數(shù)信息message 尝抖,以 handlerName 為key ,從之前 oc 中保存的方法字典中取出對應(yīng)方法迅皇,并執(zhí)行昧辽。
WVJBResponseCallback responseCallback = _responseCallbacks[message[@"responseId"]]; // 獲取存儲的callback函數(shù)
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; // 獲取存儲的handler函數(shù)
if (!handler) {
NSLog(@"oc環(huán)境中沒有注冊過此方法");
continue;
}
handler(message[@"data"], responseCallback);
總結(jié)
OC —>JS : js注冊好 functionNameIdentitify , oc 組裝好 {functionNameIdentitify, data登颓, callback} 字典搅荞,再轉(zhuǎn)化成字符串后,通過系統(tǒng)方法執(zhí)行js代碼框咙,js函數(shù)里去解析傳入的json串咕痛,獲取functionName等數(shù)據(jù),從緩存中取出對應(yīng)函數(shù)喇嘱。
JS —> OC : 原理類似茉贡,oc 注冊好 functionNameIdentitify 后 , 當(dāng)html的<iframe>request后者铜, 系統(tǒng)方法 decidePolicyForNavigationAction 會被執(zhí)行腔丧,獲取到當(dāng)前操作的request.url放椰。 攔截到是js發(fā)起的操作后, 通過系統(tǒng)方法stringByEvaluatingJavaScriptFromString 和 js方法._fetchQueue() 獲取對應(yīng)的js數(shù)據(jù)悔据; 從數(shù)據(jù)中獲取functionName 或 callbackId庄敛, 最后在oc緩存的字典中取出對應(yīng)方法執(zhí)行。
不管是OC —>JS, 還是JS —> OC 科汗, 最終都是通過系統(tǒng)的stringByEvaluatingJavaScriptFromString 或者 evaluateJavaScript 方法建立的橋梁藻烤,去獲取js數(shù)據(jù)或者傳入數(shù)據(jù)給js。
關(guān)于 js 與 oc 交互的更多細(xì)節(jié)头滔,可以自己查看WebViewJavascriptBridge的源碼 https://github.com/marcuswestin/WebViewJavascriptBridge
最后附上一段一開始被注入到webView中的JS代碼
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
var messagingIframe;
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme';
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
}
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false;
}
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;
}
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
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({
handlerName: message.handlerName,
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
handler(message.data, responseCallback);
}
}
}
}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
setTimeout(_callWVJBCallbacks, 0);
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
}