iOS與H5交互: WebViewJavascriptBridge 解析

由于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;
    }
}

這兩個地方技術實現看似完全不一樣,但是都實現了對外暴露最少的接口,模塊間盡可能解耦。

我們在開發特別是模塊化改造抽離過程中也要多多思考,不要著急下手,選取最好的實現方案。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容