由于app發(fā)版更新的限制族檬,為了快速上線,很多app會嵌入h5頁面化戳,使用h5頁面就繞不ios和h5的交互問題单料。WebViewJavascriptBridge是一個很好的解決方案埋凯。
基本技術實現(xiàn)原理:
- js向iOS通信:不能直接調用oc的方法,只能通過原生的url攔截實現(xiàn)扫尖。
- iOS向js通信:直接調用系統(tǒng)的evaluateJavaScript方法來執(zhí)行js代碼白对。
WebViewJavascriptBridge源碼:
關系:
WebViewJavascriptBridge(WKWebViewJavascriptBridge):橋接的入口,針對不同類型的 WebView (UIWebView换怖、WKWebView甩恼、WebView)進行分發(fā);執(zhí)行 JS 代碼沉颂,實現(xiàn)不同WebView的代理方法条摸,并通過攔截 URL 來通知 WebViewJavascriptBridgeBase 做相應操作
WebViewJavascriptBridgeBase:用來進行 bridge 初始化和消息處理的核心類;WKWebView出現(xiàn)后獨立出來的
WebViewJavascriptBridge_JS:一堆字符串铸屉,用于給js注入钉蒲,JS 端負責“收發(fā)消息”的代碼
具體實現(xiàn):
1、初始化
//初始化彻坛,根據(jù)傳入的參數(shù)不同返回不同類型的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環(huán)境注冊的方法顷啼,key是方法名,value是這個方法對應的回調block
//startupMessageQueue用于保存是實話過程中需要發(fā)送給javascirpt環(huán)境的消息小压。
//responseCallbacks用于保存OC于javascript環(huán)境相互調用的回調模塊线梗。通過_uniqueId加上時間戳來確定每個調用的回調椰于。
- (id)init {
if (self = [super init]) {
self.messageHandlers = [NSMutableDictionary dictionary];
self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;
}
return self;
}
js中初始化和注冊方法
//初始化 這段代碼的意思就是執(zhí)行加載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函數(shù)怠益,并且這個函數(shù)傳入的callback也是一個函數(shù)。callback函數(shù)中有我們在javascript環(huán)境中注冊的OC調用JS提供的方法方法瘾婿。
setupWebViewJavascriptBridge(function(bridge) {
/*JS給ObjC提供的API蜻牢,在ObjC端可以手動調用JS的這個API。接收ObjC傳過來的參數(shù)偏陪,且可以回調ObjC*/
bridge.registerHandler('getUserInfo', function(data, responseCallback) {
showMsg("從OC傳過來的參數(shù): ", 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調用,并且把他的回調實現(xiàn)保存在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為要傳遞的業(yè)務數(shù)據(jù),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環(huán)境的消息恳邀,如果是__wvjb_queue_message__則表示是發(fā)送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方法并執(zhí)行
- (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];
//執(zhí)行js
if ([[NSThread currentThread] isMainThread]) {
[self _evaluateJavascript:javascriptCommand];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[self _evaluateJavascript:javascriptCommand];
});
}
}
//WebViewJavascriptBridge_JS中代碼 根據(jù)handler找到該執(zhí)行的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進行管理和分發(fā)乳附,但是對外界只提供一個方法,把這三個的不同處理隱藏在自己的實現(xiàn)內部。符合設計模式迪米特法則赋除。
迪米特法則定義:
一個對象應該對其他對象有最少的了解阱缓,通俗的說,就是一個類應該對外暴露盡量少的公共接口举农,如有必要茬祷,可以把對象之間的耦合度降到最低。
迪米特法則的優(yōu)點:
1.一個類暴露的公用接口越少并蝗,那么后期修改時涉及的面就越小祭犯,由于修改造成的風險也會降到最低。
2.類之間解耦了滚停,獨立性也會相應的提升沃粗。那么類的復用率就會大大提高。
2键畴、無論是js調用原生還是原生調用js最盅,都需要在bridge中預先注冊自己的方法,提供給別人調用起惕。所以說每一個js和每一個oc方法都要進行一次注冊涡贱。真實項目中如果交互很多的話,會產(chǎn)生大量的注冊惹想。而且這個注冊是強依賴的问词,注冊和調用的地方必須一致。這樣的話oc和h5這兩個系統(tǒng)緊密耦合在一起嘀粱,不符合設計模式中要求的低耦合性激挪。
怎么辦呢?
只注冊一個handler锋叨。把具體的方法名當做參數(shù)傳垄分。對JS參數(shù)進行解析,并使用Runtime分發(fā)
//調用的入口
[self.bridge registerHandler:@"WebViewJavascriptBridgeRun" handler:^(id data, WVJBResponseCallback responseCallback) {
HDFAppLog(@"$$$$$ Javascript傳遞數(shù)據(jù): %@", data);
[weakObject p_disposeJSCallWithData:data callBack:responseCallback];
}];
/**
對JS參數(shù)進行解析娃磺,并使用Runtime分發(fā)
@param data 參數(shù)數(shù)據(jù)
@param responseCallback 回調Block
*/
- (void)p_disposeJSCallWithData:(id)data callBack:(WVJBResponseCallback)responseCallback {
if (kIsInvalidDict(data)) { //參數(shù)缺失
if (responseCallback) {
responseCallback([self hdf_returnMessageWithCode:@"411" message:@"參數(shù)缺失" 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;
}
}
這兩個地方技術實現(xiàn)看似完全不一樣,但是都實現(xiàn)了對外暴露最少的接口偷卧,模塊間盡可能解耦豺瘤。
我們在開發(fā)特別是模塊化改造抽離過程中也要多多思考,不要著急下手涯冠,選取最好的實現(xiàn)方案炉奴。