WebViewJavascriptBridge源码学习

WebViewJavascriptBridge

可以从github上看一下库的简介,这是一个iOS/OSX上,用于WKWebView和UIWebView的让Obj-CJavaScript相互发送消息(交互)的桥接库。

我们clone源码,直接进入主题。以下将基于源码中的 /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];
}

创建webView,enableLogging 方法用于打开调试信息。 bridgeForWebView 方法中创建了 WebViewJavascriptBridge 实例,并设置webView的代理给self。着重看一下 registerHandler:handler: 方法。

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

messageHandlers是一个可变字典,key为要注册的事件名称,value为该事件的具体执行。也就是,当js调用这个已经注册的handlerName事件时,会执行OC中handler闭包的内容。这也就实现了JS到OC的调用。

2.JS调用OC流程

## 假设webview页面上有一个按钮,点击后调用native方法。看一下js代码:

var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
  callbackButton.innerHTML = '点击我,我会调用oc的方法'
  callbackButton.onclick = function(e) {
   e.preventDefault()
                                 
   bridge.callHandler('loginAction', {'userId':'110','name': 'mcy'}, function(response) {
                 alert('收到oc过来的回调:'+response)
   })
  }

主要用到了js文件中的callHandler方法,它主要调用了 _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;
 }

前文已经提过,对于每一个JS和OC交互的事件,OC都会向bridge库注册。当JS调用某一事件时,会调用到function _doSend(message, responseCallback){}方法:

  • a.如果webview需要回调,即native方法执行后需要回调给webview,我们还需要给message附加一些额外信息。首先,根据当前时间生成一个callbackId,并以它为key值,将回调函数存放到responseCallbacks散列表中,目的是客户端能取到这个回调方法并执行它。
  • b.把传参message存入sendMessageQueue中(sendMessageQueue:就是一个数组,里面存放了交互所需的事件名称)。
  • c.所有准备都做好后,再修改iframe实例的src。

并不是有了 WebViewJavascriptBridge 才能实现web与native的交互。在webview的代理方法中拦截URL Scheme,对指定的URL Scheme进行处理也可以实现交互。你最好对url scheme了解多一些。
WebViewJavascriptBridge也是做了同样的事。它生成了一个特定的scheme,便于客户端拦截的时候识别它。

## 客户端拦截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会判断url是否是指定的scheme,然后区分是load型url还是message型url。
1)load型:注入javascript文件。
计算机学科里有一句名言:所有计算机中的问题,都能用添加一个中间层解决。我们能让OC和JS两种语言完成交互,主要是靠添加了一个中间层。所以,加载完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()其实是一个js文件,老的版本是写在txt里的,后来放在.m文件中,声明为一个string变量了。声明如下,#define的写法是为了免去换行要加\的烦恼。_evaluateJavascript会去执行这个js文件。

  • b.startupMessageQueue 顾名思义就是存放消息事件的队列,从中取出每一个事件,一一分发。_dispatchMessage后文再分析。

我个人认为,这个JavaScript中间层就是一个中间调度层,让2种语言在这里改装适配之后能被对方识别

2)message型:
先看:

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

它会去调用js文件中的_fetchQueue(),如下:

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

sendMessageQueue 是一个消息队列,存放web中需要调用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解析。然后遍历每一个事件:
1)有responseId:从_responseCallbacks散列表中取出响应事件的回调作为responseCallback。
2)无responseId,有callbackId:生成一个新的responseCallback。
生成新的responseCallback,从messageHandlers中取出handlerName对应的事件(最开始我们registerHandler时注册进去的),执行该事件的回调。
到这里,JS就成功调用了OC的方法。我们还可以在回调中调用 responseCallback 告诉JS它调用成功了。

我们先看一下新生成的 responseCallback 里面做了什么事情。它是怎么"联系"上JS的。

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

queueMessage 最后调用了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];
        });
    }
}

回传参数被编码为json格式,然后在主线程中调用了JS文件中的 _handleMessageFromObjC('%@') 方法。而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);
    }
   }
  }
 }

在 _doDispatchMessageFromObjC 中,如果有responseId,取出该responseId对应的responseCallback,并执行(即web页面的callHandler回调)。

OC调用JS流程

前面JS调用OC讲了很多代码,估计一时半会儿很难消化。别担心,后面的内容其实是相似的。native调用bridge的callHandler方法,这个方法的实现在bridge的.m文件中(JS调用native时也会调用callHandler,这个方法的实现在bridge的JS文件中)。它会去调用 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];
}

这个方法最后还是调用了 dispatchMessage 去执行JS文件中的 handleMessageFromObjC 方法。这个方法做的事情和OC里的flushMessageQueue做的事情是相同的。

回头对比一下2个核心bridge类的方法

OC: WebViewJavascriptBridgeBase(WebViewJavascriptBridge将其封装了一份,便于我们使用它的功能)
JS: WebViewJavascriptBridge_JS(内部就是一个js方法)

1)交互前都需要调用registerHandler,在各自的messageHandlers散列表里存放事件和信息。
2)调用另一端的时候,都要调用callHandler,分别触发 sendData 和 _doSend 方法。将各自的回调信息存放在responseCallbacks散列表中。
不同的是,OC调用JS时,可以直接调用JS文件中的 handleMessageFromObjC 方法。而JS调用OC就没那么直接了。JS的doSend改变了iframe的src后,需要在webview的代理方法shouldStartLoadWithRequest中截取url scheme,最终调用flushMessageQueue方法处理。

相信到这里之后,对于WebViewJavaScriptBridge是如何处理webview和js的交互,你已经有了一个大体的了解了。当然,有一些内容还是可以去细想的。
这种实现并不是唯一的,但整体思路都是一致的。我看了一下微信的JS桥接文件,写法和WebViewJavascriptBridge_JS略有不同,后续再研究研究~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容