WebViewJavascriptBridge源碼學(xué)習(xí)

WebViewJavascriptBridge

可以從github上看一下庫(kù)的簡(jiǎn)介,這是一個(gè)iOS/OSX上,用于WKWebView和UIWebView的讓Obj-CJavaScript相互發(fā)送消息(交互)的橋接庫(kù)。

我們clone源碼,直接進(jìn)入主題。以下將基于源碼中的 /Example/ExampleApp-iOS 工程做分析

1.客戶端使用

ExampleUIWebViewController

- (void)viewWillAppear:(BOOL)animated {
    if (_bridge) { return; }
    
    UIWebView* webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:webView];
    
    [WebViewJavascriptBridge enableLogging];
    
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    [_bridge setWebViewDelegate:self];
    
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];
    
    [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
    
    [self renderButtons:webView];
    [self loadExamplePage:webView];
}

創(chuàng)建webView田轧,enableLogging 方法用于打開調(diào)試信息。 bridgeForWebView 方法中創(chuàng)建了 WebViewJavascriptBridge 實(shí)例鞍恢,并設(shè)置webView的代理給self傻粘。著重看一下 registerHandler:handler: 方法。

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

messageHandlers是一個(gè)可變字典帮掉,key為要注冊(cè)的事件名稱弦悉,value為該事件的具體執(zhí)行。也就是蟆炊,當(dāng)js調(diào)用這個(gè)已經(jīng)注冊(cè)的handlerName事件時(shí)稽莉,會(huì)執(zhí)行OC中handler閉包的內(nèi)容。這也就實(shí)現(xiàn)了JS到OC的調(diào)用涩搓。

2.JS調(diào)用OC流程

## 假設(shè)webview頁(yè)面上有一個(gè)按鈕污秆,點(diǎn)擊后調(diào)用native方法后室。看一下js代碼:

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
  callbackButton.innerHTML = '點(diǎn)擊我,我會(huì)調(diào)用oc的方法'
  callbackButton.onclick = function(e) {
   e.preventDefault()
                                 
   bridge.callHandler('loginAction', {'userId':'110','name': 'mcy'}, function(response) {
                 alert('收到oc過來的回調(diào):'+response)
   })
  }

主要用到了js文件中的callHandler方法混狠,它主要調(diào)用了 _doSend 方法:

function _doSend(message, responseCallback) {
  if (responseCallback) {
   var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
   responseCallbacks[callbackId] = responseCallback;
   message['callbackId'] = callbackId;
  }
  sendMessageQueue.push(message);
  messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
 }

前文已經(jīng)提過,對(duì)于每一個(gè)JS和OC交互的事件疾层,OC都會(huì)向bridge庫(kù)注冊(cè)将饺。當(dāng)JS調(diào)用某一事件時(shí),會(huì)調(diào)用到function _doSend(message, responseCallback){}方法:

  • a.如果webview需要回調(diào)痛黎,即native方法執(zhí)行后需要回調(diào)給webview予弧,我們還需要給message附加一些額外信息。首先湖饱,根據(jù)當(dāng)前時(shí)間生成一個(gè)callbackId掖蛤,并以它為key值,將回調(diào)函數(shù)存放到responseCallbacks散列表中井厌,目的是客戶端能取到這個(gè)回調(diào)方法并執(zhí)行它蚓庭。
  • b.把傳參message存入sendMessageQueue中(sendMessageQueue:就是一個(gè)數(shù)組,里面存放了交互所需的事件名稱)仅仆。
  • c.所有準(zhǔn)備都做好后器赞,再修改iframe實(shí)例的src。

并不是有了 WebViewJavascriptBridge 才能實(shí)現(xiàn)web與native的交互墓拜。在webview的代理方法中攔截URL Scheme港柜,對(duì)指定的URL Scheme進(jìn)行處理也可以實(shí)現(xiàn)交互。你最好對(duì)url scheme了解多一些咳榜。
WebViewJavascriptBridge也是做了同樣的事夏醉。它生成了一個(gè)特定的scheme,便于客戶端攔截的時(shí)候識(shí)別它涌韩。

## 客戶端攔截scheme做了什么事情呢畔柔?

- (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;
    }
}

方法中isWebViewJavascriptBridgeURL會(huì)判斷url是否是指定的scheme,然后區(qū)分是load型url還是message型url贸辈。
1)load型:注入javascript文件释树。
計(jì)算機(jī)學(xué)科里有一句名言:所有計(jì)算機(jī)中的問題,都能用添加一個(gè)中間層解決擎淤。我們能讓OC和JS兩種語(yǔ)言完成交互奢啥,主要是靠添加了一個(gè)中間層。所以嘴拢,加載完webview之后桩盲,我們需要把這份JavaScript代碼注入。

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
  • a.WebViewJavascriptBridge_js()其實(shí)是一個(gè)js文件席吴,老的版本是寫在txt里的赌结,后來放在.m文件中捞蛋,聲明為一個(gè)string變量了。聲明如下柬姚,#define的寫法是為了免去換行要加\的煩惱拟杉。_evaluateJavascript會(huì)去執(zhí)行這個(gè)js文件。

  • b.startupMessageQueue 顧名思義就是存放消息事件的隊(duì)列量承,從中取出每一個(gè)事件搬设,一一分發(fā)。_dispatchMessage后文再分析撕捍。

我個(gè)人認(rèn)為拿穴,這個(gè)JavaScript中間層就是一個(gè)中間調(diào)度層,讓2種語(yǔ)言在這里改裝適配之后能被對(duì)方識(shí)別忧风。

2)message型:
先看:

NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];

它會(huì)去調(diào)用js文件中的_fetchQueue()默色,如下:

function _fetchQueue() {
  var messageQueueString = JSON.stringify(sendMessageQueue);
  sendMessageQueue = [];
  return messageQueueString;
 }

sendMessageQueue 是一個(gè)消息隊(duì)列,存放web中需要調(diào)用native的事件消息狮腿。
flushMessageQueue: 方法自然就是處理拿到的事件腿宰。如下:

- (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);
        }
    }
}

它先用 _deserializeMessageJSON 做了一下json解析。然后遍歷每一個(gè)事件:
1)有responseId:從_responseCallbacks散列表中取出響應(yīng)事件的回調(diào)作為responseCallback蚤霞。
2)無responseId酗失,有callbackId:生成一個(gè)新的responseCallback。
生成新的responseCallback昧绣,從messageHandlers中取出handlerName對(duì)應(yīng)的事件(最開始我們r(jià)egisterHandler時(shí)注冊(cè)進(jìn)去的)规肴,執(zhí)行該事件的回調(diào)。
到這里夜畴,JS就成功調(diào)用了OC的方法拖刃。我們還可以在回調(diào)中調(diào)用 responseCallback 告訴JS它調(diào)用成功了。

我們先看一下新生成的 responseCallback 里面做了什么事情贪绘。它是怎么"聯(lián)系"上JS的兑牡。

responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _queueMessage:msg];
                };

queueMessage 最后調(diào)用了dispatchMessage,如下:

- (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];
        });
    }
}

回傳參數(shù)被編碼為json格式税灌,然后在主線程中調(diào)用了JS文件中的 _handleMessageFromObjC('%@') 方法均函。而JS文件中最終被執(zhí)行的函數(shù)為:

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);
    }
   }
  }
 }

在 _doDispatchMessageFromObjC 中,如果有responseId菱涤,取出該responseId對(duì)應(yīng)的responseCallback苞也,并執(zhí)行(即web頁(yè)面的callHandler回調(diào))。

OC調(diào)用JS流程

前面JS調(diào)用OC講了很多代碼粘秆,估計(jì)一時(shí)半會(huì)兒很難消化如迟。別擔(dān)心,后面的內(nèi)容其實(shí)是相似的。native調(diào)用bridge的callHandler方法殷勘,這個(gè)方法的實(shí)現(xiàn)在bridge的.m文件中(JS調(diào)用native時(shí)也會(huì)調(diào)用callHandler此再,這個(gè)方法的實(shí)現(xiàn)在bridge的JS文件中)。它會(huì)去調(diào)用 sendData:responseCallback: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];
}

這個(gè)方法最后還是調(diào)用了 dispatchMessage 去執(zhí)行JS文件中的 handleMessageFromObjC 方法输拇。這個(gè)方法做的事情和OC里的flushMessageQueue做的事情是相同的。

回頭對(duì)比一下2個(gè)核心bridge類的方法

OC: WebViewJavascriptBridgeBase(WebViewJavascriptBridge將其封裝了一份贤斜,便于我們使用它的功能)
JS: WebViewJavascriptBridge_JS(內(nèi)部就是一個(gè)js方法)

1)交互前都需要調(diào)用registerHandler淳附,在各自的messageHandlers散列表里存放事件和信息。
2)調(diào)用另一端的時(shí)候蠢古,都要調(diào)用callHandler,分別觸發(fā) sendData 和 _doSend 方法别凹。將各自的回調(diào)信息存放在responseCallbacks散列表中草讶。
不同的是,OC調(diào)用JS時(shí)炉菲,可以直接調(diào)用JS文件中的 handleMessageFromObjC 方法堕战。而JS調(diào)用OC就沒那么直接了。JS的doSend改變了iframe的src后拍霜,需要在webview的代理方法shouldStartLoadWithRequest中截取url scheme嘱丢,最終調(diào)用flushMessageQueue方法處理。

相信到這里之后祠饺,對(duì)于WebViewJavaScriptBridge是如何處理webview和js的交互越驻,你已經(jīng)有了一個(gè)大體的了解了。當(dāng)然道偷,有一些內(nèi)容還是可以去細(xì)想的缀旁。
這種實(shí)現(xiàn)并不是唯一的,但整體思路都是一致的勺鸦。我看了一下微信的JS橋接文件并巍,寫法和WebViewJavascriptBridge_JS略有不同,后續(xù)再研究研究~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末换途,一起剝皮案震驚了整個(gè)濱河市懊渡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌军拟,老刑警劉巖剃执,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異吻谋,居然都是意外死亡忠蝗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門漓拾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阁最,“玉大人戒祠,你說我怎么就攤上這事∷僦郑” “怎么了姜盈?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)配阵。 經(jīng)常有香客問我馏颂,道長(zhǎng),這世上最難降的妖魔是什么棋傍? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任救拉,我火速辦了婚禮,結(jié)果婚禮上瘫拣,老公的妹妹穿的比我還像新娘亿絮。我一直安慰自己,他們只是感情好麸拄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布派昧。 她就那樣靜靜地躺著,像睡著了一般拢切。 火紅的嫁衣襯著肌膚如雪蒂萎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天淮椰,我揣著相機(jī)與錄音五慈,去河邊找鬼。 笑死主穗,一個(gè)胖子當(dāng)著我的面吹牛豺撑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播黔牵,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼聪轿,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了猾浦?” 一聲冷哼從身側(cè)響起陆错,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎金赦,沒想到半個(gè)月后音瓷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡夹抗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年绳慎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡杏愤,死狀恐怖靡砌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情珊楼,我是刑警寧澤通殃,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站厕宗,受9級(jí)特大地震影響画舌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜已慢,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一曲聂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧佑惠,春花似錦句葵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)剂碴。三九已至把将,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忆矛,已是汗流浹背察蹲。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留催训,地道東北人洽议。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像漫拭,于是被迫代替她去往敵國(guó)和親亚兄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容