iOS與H5交互: WebViewJavascriptBridge 解析

由于app發(fā)版更新的限制族檬,為了快速上線,很多app會嵌入h5頁面化戳,使用h5頁面就繞不ios和h5的交互問題单料。WebViewJavascriptBridge是一個很好的解決方案埋凯。

基本技術實現(xiàn)原理:

  • js向iOS通信:不能直接調用oc的方法,只能通過原生的url攔截實現(xiàn)扫尖。
  • iOS向js通信:直接調用系統(tǒng)的evaluateJavaScript方法來執(zhí)行js代碼白对。

WebViewJavascriptBridge源碼:

image.png

關系:
未命名文件-2.png

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)方案炉奴。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市蛇更,隨后出現(xiàn)的幾起案子瞻赶,更是在濱河造成了極大的恐慌赛糟,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砸逊,死亡現(xiàn)場離奇詭異璧南,居然都是意外死亡,警方通過查閱死者的電腦和手機师逸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進店門司倚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人篓像,你說我怎么就攤上這事动知。” “怎么了员辩?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵盒粮,是天一觀的道長。 經(jīng)常有香客問我奠滑,道長丹皱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任宋税,我火速辦了婚禮摊崭,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好帘靡,可當我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著阔墩,像睡著了一般嘿架。 火紅的嫁衣襯著肌膚如雪瓶珊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天耸彪,我揣著相機與錄音伞芹,去河邊找鬼。 笑死蝉娜,一個胖子當著我的面吹牛唱较,可吹牛的內容都是我干的。 我是一名探鬼主播召川,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼南缓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了荧呐?” 一聲冷哼從身側響起汉形,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤纸镊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后概疆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逗威,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年岔冀,在試婚紗的時候發(fā)現(xiàn)自己被綠了凯旭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡使套,死狀恐怖罐呼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情侦高,我是刑警寧澤弄贿,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站矫膨,受9級特大地震影響差凹,放射性物質發(fā)生泄漏。R本人自食惡果不足惜侧馅,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一危尿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧馁痴,春花似錦谊娇、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至小渊,卻和暖如春法褥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酬屉。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工半等, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人呐萨。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓杀饵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親谬擦。 傳聞我的和親對象是個殘疾皇子切距,可洞房花燭夜當晚...
    茶點故事閱讀 43,580評論 2 349

推薦閱讀更多精彩內容