由于app發版更新的限制,為了快速上線,很多app會嵌入h5頁面,使用h5頁面就繞不ios和h5的交互問題。WebViewJavascriptBridge是一個很好的解決方案。
基本技術實現原理:
- js向iOS通信:不能直接調用oc的方法,只能通過原生的url攔截實現。
- iOS向js通信:直接調用系統的evaluateJavaScript方法來執行js代碼。
WebViewJavascriptBridge源碼:
image.png
關系:
未命名文件-2.png
WebViewJavascriptBridge(WKWebViewJavascriptBridge):橋接的入口,針對不同類型的 WebView (UIWebView、WKWebView、WebView)進行分發;執行 JS 代碼,實現不同WebView的代理方法,并通過攔截 URL 來通知 WebViewJavascriptBridgeBase 做相應操作
WebViewJavascriptBridgeBase:用來進行 bridge 初始化和消息處理的核心類;WKWebView出現后獨立出來的
WebViewJavascriptBridge_JS:一堆字符串,用于給js注入,JS 端負責“收發消息”的代碼
具體實現:
1、初始化
//初始化,根據傳入的參數不同返回不同類型的bridge(UI/WK)
+ (instancetype)bridgeForWebView:(id)webView {
return [self bridge:webView];
}
+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}
#endif
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
return bridge;
}
[NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
return nil;
}
//base初始化
//messageHandlers用于保存OC環境注冊的方法,key是方法名,value是這個方法對應的回調block
//startupMessageQueue用于保存是實話過程中需要發送給javascirpt環境的消息。
//responseCallbacks用于保存OC于javascript環境相互調用的回調模塊。通過_uniqueId加上時間戳來確定每個調用的回調。
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
js中初始化和注冊方法
//初始化 這段代碼的意思就是執行加載WebViewJavascriptBridge_JS.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)
}
//調用setupWebViewJavascriptBridge函數,并且這個函數傳入的callback也是一個函數。callback函數中有我們在javascript環境中注冊的OC調用JS提供的方法方法。
setupWebViewJavascriptBridge(function(bridge) {
/*JS給ObjC提供的API,在ObjC端可以手動調用JS的這個API。接收ObjC傳過來的參數,且可以回調ObjC*/
bridge.registerHandler('getUserInfo', function(data, responseCallback) {
showMsg("從OC傳過來的參數: ", data)
responseCallback({'userId': '123456', 'name': 'huiwang227'})
})
document.getElementById('clickBtn').onclick = function (e) {
bridge.callHandler('getResultObjC', {'toOC': 'xxxxxx'}, function(response) {
showMsg('OC的返回值', response)
})
}
})
2、OC中注冊
[self.bridge registerHandler:@"getResultObjC" handler:^(id data, WVJBResponseCallback responseCallback) {
NSDictionary *dic = (NSDictionary *)data;
NSString *msg = [dic objectForKey:@"toOC"];
NSLog(@"---------toOC--------%@",msg);
if (responseCallback) {
// 反饋給JS
responseCallback(@{@"result": @"oc返回的結果"});
}
}];
//注冊一個OC方法OC提供方法給JS調用給javascript調用,并且把他的回調實現保存在messageHandlers中。
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
_base.messageHandlers[handlerName] = [handler copy];
}
3、js調用原生:
//js方法
bridge.callHandler('getResultObjC', {'toOC': 'xxxxxx'}, function(response) {
showMsg('OC的返回值', response)
})
//WebViewJavascriptBridge_js 中代碼
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback = data;
data = null;
}
_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;
}
//message為要傳遞的業務數據,QUEUE_HAS_MESSAGE為oc中url攔截的標志
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
//攔截url oc中的代碼 通過[_base isWebViewJavascriptBridgeURL:url]來判斷是否是普通的跳轉還是webViewjavascriptBridege的跳轉。
//如果是__bridge_loaded__表示是初始化javascript環境的消息,如果是__wvjb_queue_message__則表示是發送javascript消息。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}
//通過handler尋找注冊過的oc方法并執行
- (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;
}
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);
}
}
}
4、原生調js
[self.bridge callHandler:@"getUserInfo" data:@{@"userID": @"12345"} responseCallback:^(id responseData) {
NSLog(@"from js: %@", responseData);
}];
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
[_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
- (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];
}
- (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"];
//在js中加入WebViewJavascriptBridge方法
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
//執行js
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
//WebViewJavascriptBridge_JS中代碼 根據handler找到該執行的js
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);
}
}
}
}
總結和思考:
1、bridge分別對(UIWebView、WKWebView、WebView)三種webview進行管理和分發,但是對外界只提供一個方法,把這三個的不同處理隱藏在自己的實現內部。符合設計模式迪米特法則。
迪米特法則定義:
一個對象應該對其他對象有最少的了解,通俗的說,就是一個類應該對外暴露盡量少的公共接口,如有必要,可以把對象之間的耦合度降到最低。
迪米特法則的優點:
1.一個類暴露的公用接口越少,那么后期修改時涉及的面就越小,由于修改造成的風險也會降到最低。
2.類之間解耦了,獨立性也會相應的提升。那么類的復用率就會大大提高。
2、無論是js調用原生還是原生調用js,都需要在bridge中預先注冊自己的方法,提供給別人調用。所以說每一個js和每一個oc方法都要進行一次注冊。真實項目中如果交互很多的話,會產生大量的注冊。而且這個注冊是強依賴的,注冊和調用的地方必須一致。這樣的話oc和h5這兩個系統緊密耦合在一起,不符合設計模式中要求的低耦合性。
怎么辦呢?
只注冊一個handler。把具體的方法名當做參數傳。對JS參數進行解析,并使用Runtime分發
//調用的入口
[self.bridge registerHandler:@"WebViewJavascriptBridgeRun" handler:^(id data, WVJBResponseCallback responseCallback) {
HDFAppLog(@"$$$$$ Javascript傳遞數據: %@", data);
[weakObject p_disposeJSCallWithData:data callBack:responseCallback];
}];
/**
對JS參數進行解析,并使用Runtime分發
@param data 參數數據
@param responseCallback 回調Block
*/
- (void)p_disposeJSCallWithData:(id)data callBack:(WVJBResponseCallback)responseCallback {
if (kIsInvalidDict(data)) { //參數缺失
if (responseCallback) {
responseCallback([self hdf_returnMessageWithCode:@"411" message:@"參數缺失" data:nil]);
}
return;
}
NSString *actionName = [NSString stringWithFormat:@"hdf_%@:", [data hdf_safeObjectForKey:@"nativeMethod"]];
//版本不支持
if (kIsEmptyString(actionName)) {
if (responseCallback) {
responseCallback([self hdf_returnMessageWithCode:@"410" message:@"版本不支持" data:nil]);
}
return;
}
NSMutableDictionary *params = [NSMutableDictionary dictionary];
[params hdf_setSafeObject:[data hdf_safeObjectForKey:@"data"] forKey:@"data"];
[params hdf_setSafeObject:responseCallback forKey:@"retBlock"];
SEL action = NSSelectorFromString(actionName);
if ([self respondsToSelector:action])
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:action withObject:params];
return;
#pragma clang diagnostic pop
}
else //無響應,跳轉到一個公用錯誤頁面/返回nil
{
if (responseCallback) {
responseCallback([self hdf_returnMessageWithCode:@"410" message:nil data:nil]);
}
return;
}
}
這兩個地方技術實現看似完全不一樣,但是都實現了對外暴露最少的接口,模塊間盡可能解耦。
我們在開發特別是模塊化改造抽離過程中也要多多思考,不要著急下手,選取最好的實現方案。