美文网首页
源码分析 - WebViewJavascriptBridge

源码分析 - WebViewJavascriptBridge

作者: mtry | 来源:发表于2020-05-24 13:48 被阅读0次

主要作用

WebViewJavascriptBridge 是一个 OC 与 JS 消息分发工具。OC 可以通过向 WebViewJavascriptBridge 监听 JS 的消息,也可以向 JS 主动发送消息;JS 也同样支持监听 OC 消息和主动向 OC 发送消息。

基础原理

OC to JS

主要通过 WKWebView 的实例方法

- (void)evaluateJavaScript:(NSString *)javaScriptString 
         completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable))completionHandler;

比如:通过 JS 调用 UIAlertController 弹窗。

- (void)test {
    [self.webView evaluateJavaScript:@"alert(\"Testing Alerts\");"
                   completionHandler:^(id _Nullable res, NSError * _Nullable error) {
        NSLog(@"%@ %@", res, error);
    }];
}

注意:js 调用 alert 方法,会触发 WKUIDelegate 协议的 webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler: 方法,需要在方法中实现对应的弹窗逻辑。

通过这种方式,可以做到 OC 主动向 JS 发送消息,并且还可以收到来着 JS 的回调。

JS to OC

js 通过修改 iframe 的 src 来触发 OC 的 WKNavigationDelegate 协议,比如

JS 触发

var WVJBIframe = document.createElement('iframe');
WVJBIframe.src = 'https://__bridge_loaded__';

OC 回调

- (void)webView:(WKWebView *)webView 
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
                decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    NSURL *url = navigationAction.request.URL;
    NSLog(@"url:%@", url);
}

/*
打印如下:
url:https://__bridge_loaded__
*/

小结

基于上面介绍 OC to JSJS to OC 的原理,我们可以自己封装一个 JS 与 OC 消息分发中心。当然 WebViewJavascriptBridge 已经封装的很好了,也没啥必要重复造轮子,如果业务上只是简单的需要 JS 与 OC 通信,就可以基于上面原理来实现即可。

注意

iframe 的 src 有长度限制,所以 JS 向 OC 发送消息,如果通过 src 来传递,是有限制的。为了避免这种情况,我们可以通过约定一个的 src 告诉 OC,JS 要跟 OC 发送消息了,然后在 webView:decidePolicyForNavigationAction:decisionHandler 收到回调之后,通过 WKWebviewevaluateJavaScript:completionHandler 方法来获取 JS 向 OC 要发送的消息数据。

WebViewJavascriptBridge 底层设计

下面就是 WebViewJavascriptBridge 的通信过程。

注意:由于消息的传输都是基于 WKWebView 来实现的,都是在主线程上实现的,所以就不需要考虑多线程问题。

Snip20200524_1.png
  • OC业务层:OC相关业务开发
  • OC接口层:WKWebViewJavascriptBridgeWebViewJavascriptBridge 分别对应 WKWebViewUIWebView
  • 解析层:WebViewJavascriptBridgeBase
  • JS接口层:WebViewJavascriptBridge_JS
  • JS业务层:JS相关业务开发

接下来主要讨论应用 WKWebView 的情况,UIWebView 实现类似。

分析 OC 向 JS 发送消息过程

OC 向 JS 发送消息,我们以下为例子来分析。

// 业务调用OC接口层
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
    NSLog(@"testJavascriptHandler responded: %@", response);
}];

// 解析层转换后,通过 WKWebView.evaluateJavaScript 调用
NSString *javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
[_webView evaluateJavaScript:javascriptCommand completionHandler:nil]

// JS接口层收到响应
function _handleMessageFromObjC(messageJSON) {
    //...
}

由于向 JS 发送消息之后要进行 responseCallback 回调,所以需要把 responseCallback 进行缓存起来。考虑到同一个事件有可能同时发送多个消息,所以对每次发送消息需要生成一个 id 进行标记。有了这个 id,当 JS 向 OC 发送消息时,如果消息中包含这个 id,说明这是一个需要进行 responseCallback 回调。

顺便说一下题外话,OC 向 JS 发送消息,虽然是在主线程,但是发送和回调不是同步的,所以需要用 id 对消息进行标记。

下面看一下具体实现,以下为简化之后的逻辑

- (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 _dispatchMessage:message];
}

- (void)_dispatchMessage:(NSMutableDictionary*)message {
    NSData *messageData = [NSJSONSerialization dataWithJSONObject:message options:0 error:nil]
    NSString *messageJSON = [[NSString alloc] initWithData:messageData encoding:NSUTF8StringEncoding];
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
}

回调其实就是走了一遍 JS 向 OC 发送消息的过程,然后根据消息中是否包含标记 id 来判断。

下面分析 JS 向 OC 发送消息过程

bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
    log('JS got response', response)
})

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;
    }
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

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

可以发现,每次 JS 向 OC 发送消息的时候,都会通过修改一次 iframe 的 src,触发WKWebViewWKNavigationDelegate 协议中的函数。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            // 初始化标记
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            // JS 向 OC 发送消息标记
            [self WKFlushMessageQueue];
        } else {
            // 未知消息
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

- (void)WKFlushMessageQueue {
    NSString *javaScriptString = @"WebViewJavascriptBridge._fetchQueue();";
    [_webView evaluateJavaScript:javaScriptString completionHandler:^(NSString* result, NSError* error) {
        // result 就是 js 消息队列数据列表
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

// 下面就是拿到 js 的消息队列的分发逻辑(只保留关键逻辑)
- (void)flushMessageQueue:(NSString *)messageQueueString{
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (NSMutableDictionary* message in messages) {
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            // OC 向 JS 主动发送消息之后的回调逻辑
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            // JS 向 OC 主动发送消息,OC 监听 JS 消息逻辑
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {
                // 这里是 JS 发送消息之后的回调逻辑
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    NSMutableDictionary* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _dispatchMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            if (!handler) {
                continue;
            }
            handler(message[@"data"], responseCallback);
        }
    }
}

小结

  • OC 向 JS 发送消息:evaluateJavaScript:completionHandler:
  • JS 向 OC 发送消息:
    • 第一步:JS 修改 iframe 的 src 触发WKWebViewWKNavigationDelegate 协议中的函数
    • 第二步:OC 通过 evaluateJavaScript:completionHandler: 获取 JS 向 OC 发送的消息数据

相关文章

网友评论

      本文标题:源码分析 - WebViewJavascriptBridge

      本文链接:https://www.haomeiwen.com/subject/sezdahtx.html