篇頭語
- 本篇參考文章在章尾,博客寫的很不錯,但是需要反復詳讀加上自己在頭腦中繪制邏輯結構才能比較好的理解,不然“東一塊、西一塊”很難聯(lián)系到一起,所以我結合了較多部分上述文章以外單獨又重新總結了一遍,加上了一些自己的理解和細節(jié)上的處理,也做了一些方便理解的結構圖,方便自己“一步到位”的復習,也方便其他人理解。
- 至于為什么要選擇這個框架進行解析呢,是因為目前H5和Native的連接脫落不了“對于網(wǎng)址跳轉(zhuǎn)攔截”這一個過程,此框架使用廣泛,寫法成熟,研讀此框架不僅是為了了解這個框架,更多意義上是為什么理解Native接H5的原理,也方便了解各個框架對這方面的處理的差異化和優(yōu)勢體現(xiàn)在哪里
基本構成
對于整個框架來說分為三個部分

- oc部分,包括oc處理暴露給js接口的類:WKWebViewJavascriptBridge.m/.h
- js部分,包括js處理暴露給oc接口的文件: ExampleApp.html
- bridge處理部分,這個部分對于oc和js都各有一個文件,oc是類:WebViewJavascriptBridgeBase.m/.h,js則是:WebViewJavascriptBridge_JS.m,雖然是.m文件,但是其中是js代碼,點開就知道了。
- oc和js部分的作用就是聲明給對方調(diào)用的方法,以及提供供自身使用可以調(diào)用對方的一個接口,但是具體如何調(diào)用的js、如果調(diào)用的oc或者說如何注冊給js、如何注冊給oc使用的方法,這些邏輯都放在兩端的bridge部分進行處理。
H5端的處理過程

處理過程其實是指對于bridge的處理過程,oc端的處理都在Base.m中,流程和js的初始化原理大致相同,但是js端的過程比較繁瑣,因為初始化過程直接就來了一個和oc的交互。為什么會有這么一個交互過程呢?因為對于js的初始化代碼被存放到了移動端的文件中,這一點應該是出于盡可能的減少雙端工作量的考量吧,這樣存放雖然加大了js端初始化的復雜性,但是這對于開發(fā)者是黑盒的,所以也不能說是一個缺點。這個處理過程是一個在js端初始化一個bridge并達到通過這個bridge可以跟oc交互的目的的過程。先看一下植入WKWebViewJavascriptBridge框架的過程中,在js端所必須添加的代碼(官網(wǎng)要求復制進js的一部分代碼)
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 = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
/*與OC交互的所有JS方法都要放在此處注冊,才能調(diào)用通過JS調(diào)用OC或者讓OC調(diào)用這里的JS*/
setupWebViewJavascriptBridge(function(bridge) {
var uniqueId = 1
function log(message, data) {
var log = document.getElementById('log')
var el = document.createElement('div')
el.className = 'logLine'
el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
if (log.children.length) {
log.insertBefore(el, log.children[0])
} else {
log.appendChild(el)
}
}
//register、call代碼
bridge.registerHandler('getUserInfos', function(data, responseCallback) {
log("這是在H5中的getUserInfos 接收的從ObjC傳過來的參數(shù)", data)
responseCallback({'userId': '123456', 'blog': '標哥的技術博客'})
})
/*JS給ObjC提供公開的API,ObjC端通過注冊,就可以在JS端調(diào)用此API時,得到回調(diào)。ObjC端可以在處理完成后,反饋給JS,這樣寫就是在載入頁面完成時就先調(diào)用*/
bridge.callHandler('getUserIdFromObjC', function(responseData) {
log("這是在H5中的getUserIdFromObjC方法對應的參數(shù)responseData的值", responseData)
})
})
下面是對以上代碼進行一個簡單的簡化,解耦出一個方法callback,但是可以方便理解。所以主要看下面的代碼即可
function setupWebViewJavascriptBridge(callback) {
//第一次調(diào)用這個方法的時候,為false
if (window.WebViewJavascriptBridge) {
var result = callback(WebViewJavascriptBridge);
return result;
}
//第一次調(diào)用的時候,也是false
if (window.WVJBCallbacks) {
var result = window.WVJBCallbacks.push(callback);
return result;
}
//把callback對象賦值給對象。
window.WVJBCallbacks = [callback];
//這段代碼的意思就是執(zhí)行加載WebViewJavascriptBridge_JS.js中代碼的作用
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() {
document.documentElement.removeChild(WVJBIframe)
}, 0);
}
//setupWebViewJavascriptBridge執(zhí)行的時候傳入的參數(shù),這是一個方法。
function callback(bridge) {
var uniqueId = 1
//把WEB中要注冊的方法注冊到bridge里面
bridge.registerHandler('OC調(diào)用JS提供的方法', function(data, responseCallback) {
log('OC調(diào)用JS方法成功', data)
var responseData = { 'JS給OC調(diào)用的回調(diào)':'回調(diào)值!' }
log('OC調(diào)用JS的返回值', responseData)
responseCallback(responseData)
})
};
//驅(qū)動所有hander的初始化
setupWebViewJavascriptBridge(callback);
- 這里直接調(diào)用了setupWebViewJavascriptBridge這個方法,但是實際上調(diào)用這個方法前兩個語句都不會執(zhí)行。因為沒有初始化賦值的情況下,window的屬性都是空的,但是其他的語句是會被執(zhí)行的,就是下面的代碼。
- 單看這兩個方法setupWebViewJavascriptBridge以及callback來說的話,callback的參數(shù)bridge一直沒有被賦值,不過無所謂,在第一次調(diào)用的時候callback方法作為setupWebViewJavascriptBridge方法的參數(shù),也沒有被調(diào)用,所以此時參數(shù)為空也無所謂。
- 下面這段代碼的主要目的如下面注釋所說,但是細節(jié)上也需要了解一下,(?js實在是沒什么基礎,看起來比較吃力)代碼實際上創(chuàng)建了一個iframe,這個是一個web中再打開一個web頁面的這么一個組件,把這個組件加到頁面又移走,設置為不可見,然后這個iframe指向了一個地址為“https://bridge_loaded”的東西,其實是為了制造webview的url跳轉(zhuǎn),從而觸發(fā)oc的回調(diào)方法。
_iframe可以理解為webview中的窗口,當我們改變iframe的src屬性的時候,相當于我們?yōu)g覽器實現(xiàn)了鏈接的跳轉(zhuǎn)。比如從www.baidu.com跳轉(zhuǎn)到www.google.com。下面這段代碼的目的就是實現(xiàn)一個到https://bridge_loaded的跳轉(zhuǎn)。觸發(fā)oc回調(diào)的目的是因為為js端初始化bridge的代碼需要從oc端的回調(diào)方法中觸發(fā),從而達到初始化javascript環(huán)境的bridge的作用。
//這段代碼的意思就是執(zhí)行加載WebViewJavascriptBridge_JS.js中代碼的作用
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() {
document.documentElement.removeChild(WVJBIframe)
}, 0);
為js端初始化bridge的代碼是一個js文件,這個文件實現(xiàn)了為上述代碼中的callback方法中的bridge賦值。通過上述代碼在oc端被執(zhí)行在當前webview,至于怎么通過這個“跳轉(zhuǎn)”觸發(fā)js文件的執(zhí)行的呢?這就取決于oc這邊的方法回調(diào)了,實現(xiàn)了WKNavigationDelegate協(xié)議的類會在內(nèi)部WKWebView發(fā)生地址跳轉(zhuǎn)的時候觸發(fā)以下這個方法:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
//如果是WebViewJavascriptBridge發(fā)送或者接受的消息,則特殊處理。否則按照正常流程處理。
if ([_base isWebViewJavascriptBridgeURL:url]) {
//第一次注入JS代碼,也就是說只有在初始化的時候才會被調(diào)用一次
//是通過判斷url的scheme和host字段是不是預置的特殊含義的字段
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
//此處判斷出消息是從WEB發(fā)過來的,所有從web發(fā)來的消息都走此口,無論是“js調(diào)用oc的方法”消息,還是“oc調(diào)用js方法,js執(zhí)行完畢發(fā)回來的消息(目的是告訴oc我執(zhí)行完了,你可以執(zhí)行你設置的回調(diào)函數(shù)了?。? } else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
//下面是webview的正常代理執(zhí)行流程,不用管。
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
注意上面的代碼,很多是調(diào)用base的方法執(zhí)行,這是解耦思想呦!和bridge相關的具體操作細節(jié)要移步到BridgeBase類實現(xiàn)。下面看一下[base injectJavascriptFile];這個方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且執(zhí)行,從而達到初始化javascript環(huán)境的brige的作用。至于其他部分用到的時候再看。
//初始化的是否注入WebViewJavascriptBridge_JS.js
- (void)injectJavascriptFile {
NSString *js = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WebViewJavascriptBridge_JS.js" ofType:nil] encoding:NSUTF8StringEncoding error:nil];
//把javascript代碼注入webview中執(zhí)行,這里執(zhí)行具體的注入操作。
[self _evaluateJavascript:js];
//如果javascript環(huán)境初始化完成以后,有startupMessageQueue消息。則立即發(fā)送消息。
//startupMessageQueue置為nil,不reset情況下將不會在被賦值
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQueue;
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}
}
}
//把javascript代碼寫入webview
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
return NULL;
}
上面代碼注釋已經(jīng)很清晰了,其實就是拼接一條js指令然后通過oc提供的方式把他丟到webview中去執(zhí)行,然后看一下要執(zhí)行的這個文件,其實在下面的這段代碼目的執(zhí)行的js文件對于bridge進行了賦值,下面來看一下這個WebViewJavascriptBridgeBase_JS.js文件,也就是js端的bridge文件
;(function() {
//如果已經(jīng)初始化了,則返回。
if (window.WebViewJavascriptBridge) {
return;
}
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
}
}
//初始化一些屬性。
var messagingIframe;
//用于存儲消息列表
var sendMessageQueue = [];
//用于存儲消息
var messageHandlers = {};
//通過下面兩個協(xié)議組合來確定是否是特定的消息,然后攔擊。
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
//oc調(diào)用js的回調(diào)
var responseCallbacks = {};
//消息對應的id
var uniqueId = 1;
//是否設置消息超時
var dispatchMessagesWithTimeoutSafety = true;
//web端注冊一個消息方法
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = handler;
}
//web端調(diào)用一個OC注冊的消息
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;
}
//把消息轉(zhuǎn)換成JSON字符串返回
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueString;
}
//OC調(diào)用JS的入口方法
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
//初始化橋接對象,OC可以通過WebViewJavascriptBridge來調(diào)用JS里面的各種方法。
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC
};
//處理從OC返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
//回調(diào)
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {//主動調(diào)用
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
};
}
//獲取JS注冊的函數(shù)
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
//調(diào)用JS中的對應函數(shù)處理
handler(message.data, responseCallback);
}
}
}
}
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
//messagingIframe.body.style.backgroundColor="#0000ff";
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
document.documentElement.appendChild(messagingIframe);
//注冊_disableJavascriptAlertBoxSafetyTimeout方法,讓OC可以關閉回調(diào)超時,默認是開啟的。
registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
//執(zhí)行_callWVJBCallbacks方法
setTimeout(_callWVJBCallbacks, 0);
//初始化WEB中注冊的方法。這個方法會把WEB中的hander注冊到bridge中。
//下面的代碼其實就是執(zhí)行WEB中的callback函數(shù)。
function _callWVJBCallbacks() {
var callbacks = window.WVJBCallbacks;
delete window.WVJBCallbacks;
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](WebViewJavascriptBridge);
}
}
})();
上面代碼比較復雜,但是卻比較容易看,主要分為以下幾個功能:
- 三個屬性:
- sendMessageQueue 用于存儲消息列表
- messageHandlers 用于存儲注冊給oc調(diào)用的方法
- responseCallbacks 用于存儲oc在調(diào)用js方法時需要調(diào)用的js提供的回調(diào)方法
- 實現(xiàn)了registerHandler方法,原理是把方法名和方法實現(xiàn)存儲在messageHandlers隊列中
- 實現(xiàn)了callHandler方法,原理是生成一個callback的id,和callback的實現(xiàn)作為對用的key、value一同存儲在responseCallbacks隊列中,與此同時,把方法名和參數(shù)和callback的id存儲在一個字典中,然后吧這個字典加入到sendMessageQueue隊列中,最后變化網(wǎng)址,形成跳轉(zhuǎn),觸發(fā)oc回調(diào)方法,實現(xiàn)一個信息交流,至于是怎么把參數(shù)和方法名稱帶過去的呢?
- disableJavscriptAlertBoxSafetyTimeout等相關的一些列作用暫時沒研究?
- 實現(xiàn)了接收oc消息的方法,原理是解析收到的json值,從中解析出需要的回調(diào)函數(shù)的信息,然后觸發(fā)回調(diào)函數(shù),之后從回調(diào)函數(shù)的隊列responseCallbacks中刪除該回調(diào)方法,如果說oc返回的信息中沒有這個回調(diào)方法,是不是說oc端沒有主動回調(diào)這個js的回調(diào)?則將會通過json數(shù)據(jù)獲取到之前存儲的message的callbackid來查找對應的回調(diào)函數(shù)來進行調(diào)用,之后的步驟和callHandler同理,也就是說這個回調(diào)函數(shù)是一定會被調(diào)用的?
- 通過同樣的方式制造跳轉(zhuǎn),目的是把初始化好之后把js要發(fā)給oc的消息一下發(fā)出去
- 立即執(zhí)行的函數(shù),把WebViewJavascriptBridge注入到之前文件的callbacks方法中作為參數(shù),實現(xiàn)register方法的真正觸發(fā),(扣題)
oc環(huán)境初始化
在bridge.m中其實工作很簡單,他只是一個對于oc項目連接H5的入口,提供一個單例,搞定自己的webview和base就好了。
//初始化一個OC環(huán)境的橋WKWebViewJavascriptBridge并且初始化。
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
WKWebViewJavascriptBridge* bridge = [[self alloc] init];
//調(diào)用下面那個方法
[bridge _setupInstance:webView];
[bridge reset];
return bridge;
}
//初始化
- (void) _setupInstance:(WKWebView*)webView {
_webView = webView;
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;
}
剩余的初始化工作在BridgeBase.m
//messageHandlers用于保存OC環(huán)境注冊的方法,key是方法名,value是這個方法對應的回調(diào)block
//startupMessageQueue用于保存是實話過程中需要發(fā)送給javascirpt環(huán)境的消息。
//responseCallbacks用于保存OC于javascript環(huán)境相互調(diào)用的回調(diào)模塊。通過_uniqueId加上時間戳來確定每個調(diào)用的回調(diào)。
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
所有與javascript之間交互的信息都存儲在messageHandlers和responseCallbacks中。這兩個屬性記錄了OC環(huán)境與javascript交互的信息。
OC發(fā)消息給WEB的過程梳理

對于oc調(diào)用js的方法,走下面這個流程:
- (void)callHandler:(id)sender {
id data = @{ @"OC調(diào)用JS方法": @"OC調(diào)用JS方法的參數(shù)" };
[_bridge callHandler:@"OC調(diào)用JS提供的方法" data:data responseCallback:^(id response) {
// NSLog(@"testJavascriptHandler responded: %@", response);
}];
}
/*
handerName:OC調(diào)用JS提供的方法
data:{@"OC調(diào)用JS方法的參數(shù)":@"OC調(diào)用JS方法"}
responseCallback:回調(diào)block
*/
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
把所有信息存入一個名字為message的字典中。里面拼裝好參數(shù)data、回調(diào)IDcallbackId(現(xiàn)場生成)、要調(diào)用的js方法名字handlerName。于此同時,把剛剛生成的callbackID和callback方法存入自身的callback隊列中,目的是js執(zhí)行回來的時候再調(diào)用,具體如下,其實整個過程處理和js的bridge文件的處理方式一樣,
- (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;
}
//注意是要調(diào)用的js方法的名字
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
其次把封裝好的信息[參數(shù)data、回調(diào)IDcallbackId(現(xiàn)場生成)、要調(diào)用的js方法名字handlerName
]發(fā)送出去,把OC消息序列化、并且轉(zhuǎn)化為javascript環(huán)境的格式。然后在主線程中調(diào)用_evaluateJavascript。
- (void)_queueMessage:(WVJBMessage *)message {
//啥時候有啥時候沒有??這里好像是只會存儲js初始化文件執(zhí)行之前的消息,由于js未初始化,所以消息都加到隊列中,初始化之后以后一次集體發(fā)送,但是初始化之后隊列被置為nil,將不會再走if,都是有消息直接就發(fā)送出去
if (self.startupMessageQueue) {
//加完了就完事了嗎??
[self.startupMessageQueue addObject:message];
} else {
[self _dispatchMessage:message];
}
}
- (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];
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
- (NSString *)_serializeMessage:(id)message pretty:(BOOL)pretty {
return [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:message options:(NSJSONWritingOptions)(pretty ? NSJSONWritingPrettyPrinted : 0) error:nil] encoding:NSUTF8StringEncoding];
}
整個流程對消息進行了一系列處理,最后的處理通過一句代碼進行發(fā)送
NSString *javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
其實最終形成的字符串是
WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}');
其實就是通過javascript環(huán)境中的Bridge對象的_handleMessageFromObjC方法。下面我們?nèi)ebViewJavascriptBridge_JS.js中看_handleMessageFromObjC的處理過程。
//OC調(diào)用JS的入口方法
//messsgeJSON:{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC調(diào)用JS方法\":\"OC調(diào)用JS方法的參數(shù)\"},\"handlerName\":\"OC調(diào)用JS提供的方法\"}
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON);
}
//處理從OC返回的消息。
function _dispatchMessageFromObjC(messageJSON) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON);
var messageHandler;
var responseCallback;
//很明顯在此過程中responseId)是沒有的,所以跳過,message有三個key:data、callbackId、handleName
//這一部分是oc執(zhí)行完js的調(diào)用返回信息調(diào)用js回調(diào)的邏輯
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
}
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//這一部分是處理oc調(diào)用js方法的邏輯
//這里為什么要獲取oc的callbackID是因為要在執(zhí)行完之后給oc發(fā)送消息時附帶
if (message.callbackId) {
var callbackResponseId = message.callbackId;
//準備好方法結束后執(zhí)行的“通知oc可以執(zhí)行回調(diào)函數(shù)”的方法
responseCallback = function(responseData) {
_doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
};
}
//獲取JS注冊的函數(shù)
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
//調(diào)用JS中的對應函數(shù)處理,附帶上剛剛拼接好的回調(diào)函數(shù)
handler(message.data, responseCallback);
}
}
}
}
上面這段代碼很容易理解,而且我已經(jīng)把注釋寫的非常清晰了,其實就是通過判斷消息中是否有responseId來判斷這是一個oc的回調(diào)消息,還是說oc想要調(diào)用js的方法。js處理完方法調(diào)用之后直接調(diào)用_doSend方法把信息返回OC。下面我們看看_doSend的具體實現(xiàn):
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
//在這一步驟中if語句是不執(zhí)行的
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
在這一過程中,并沒有向_doSend傳入responseCallback參數(shù),因為這是一個“js執(zhí)行完方法,要發(fā)送消息給oc”的過程,而不是要發(fā)送“調(diào)用oc方法的消息”,所以不需要第二個參數(shù)。也是就在if語句不執(zhí)行的情況下,直接通過頁面跳轉(zhuǎn)把消息發(fā)走,但是之前封裝的message并沒有隨著這個網(wǎng)址一起發(fā)走,它被加入到了自身的sendMessageQueue隊列中,等oc收到消息后會自己來取。
其中最重要還是最后面的通過改變iframe的messagingIframe.src。從而觸發(fā)webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler從而在OC中處理javascript環(huán)境觸發(fā)過來的回調(diào)。具體如下:
if ([_base isWebViewJavascriptBridgeURL:url]) {
//第一次注入JS代碼
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
//處理WEB發(fā)過來的消息
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
}
decisionHandler(WKNavigationActionPolicyCancel);
}
這里會走[self WKFlushMessageQueue];方法。然后通過調(diào)用WebViewJavascriptBridge._fetchQueue()來獲取javascript給OC的回調(diào)信息。
//獲取WEB消息的JSON字符串
- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";
}
////把消息或者WEB回調(diào)從OC發(fā)送到OC
- (void)WKFlushMessageQueue {
NSString *js = [_base webViewJavascriptFetchQueyCommand];
[_webView evaluateJavaScript:js completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
}
//把消息或者WEB回調(diào)從OC發(fā)送到OC
[_base flushMessageQueue:result];
}];
}
獲取到javascript給OC的回調(diào)消息以后,然后把javascript的bridge返回的信息加工處理成OC環(huán)境的bridge能識別的信息。從而找到具體的實現(xiàn)執(zhí)行。
//把從WEB發(fā)送的消息返回。然后在這里處理
- (void)flushMessageQueue:(NSString *)messageQueueString{
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
return;
}
//此方法在上面有貼,用于解析json數(shù)據(jù)
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
continue;
}
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
}
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
};
} else {
responseCallback = ^(id ignoreResponseData) {
// Do nothing
};
}
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
continue;
}
handler(message[@"data"], responseCallback);
}
}
}
其實就是依然是通過判斷消息中是否有responseId來判斷這是一個“js給oc的回調(diào)消息”,還是說這是“js想調(diào)用oc的消息”。注意此時拿到的message是上一步在js中封裝的,其中包括data,callbackId和handleName
WEB發(fā)消息給OC
基于框架的作用,無論在oc端調(diào)用H5或者是在H5端調(diào)用oc都僅需含簡單的幾行代碼
bridge.callHandler('OC提供方法給JS調(diào)用',params, function(response) {
log('JS調(diào)用OC的返回值', response)
})
此處調(diào)用的事BridgeBase.js中的方法
//web端調(diào)用一個OC注冊的消息
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_doSend({ handlerName: handlerName, data: data }, responseCallback);
}
//把消息從JS發(fā)送到OC,執(zhí)行具體的發(fā)送操作。
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
//存儲消息的回調(diào)ID
responseCallbacks[callbackId] = responseCallback;
//把消息對應的回調(diào)ID和消息一起發(fā)送,以供消息返回以后使用。
message['callbackId'] = callbackId;
}
//把消息放入消息列表
sendMessageQueue.push(message);
//下面這句話會出發(fā)JS對OC的調(diào)用
//讓webview執(zhí)行跳轉(zhuǎn)操作,從而可以在
//webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中攔截到JS發(fā)給OC的消息
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
其實剩下的過程就很類似了,就不多說了