WebViewJavascriptBridge源码分析

博客链接 WebViewJavascriptBridge源码分析

在APP的开发过程中,都会通过H5来实现部分功能,H5页面是内嵌在原生应用的WebView组件中。在有的场景下,当两端需要相互通信,但是JavaScript的权限受到限制,比如不能修改系统配置等,这个时候需要委托Native去实现某个功能,并在完成后将结果通知JavaScript。所以我们需要在Native和JavaScript之间就搭建一个通信的桥梁,这个桥梁就是我们所说的JavaScript Bridge,简称 JS Bridge。

通常实现Native与JS桥接的方式有两种:

  1. 通过JavaScriptCore框架
  2. 通过Webview拦截请求的方式(WebViewJavascriptBridge使用的方式)

marcuswestin/WebViewJavascriptBridge是使用第2种方式实现在用于在WKWebView和UIWebView中,JS与Native相互发送消息。

与其他OC的三方库不同,WebViewJavascriptBridge的实现包括OC和JS两部分,因此只看OC部分的代码我们是无法理解这个bridge是如何实现两端通信的。

WebViewJavascriptBridge中的类的作用

  • WebViewJavascriptBridgeBase:OC端桥接基础服务类,维护OC端开放给JS端的方法以及OC回调方法,实现OC向JS发送数据的具体逻辑。
  • WebViewJavascriptBridge_JS:维护了一份JS代码,用于JS环境的注入。同时维护JS端的bridge对象,管理JS端注册的方法集合以及回调方法集合,面向Web端提供注册JS方法、调用OC端方法的接口。
  • WKWebViewJavascriptBridge:基于WKWebView的OC端交互逻辑处理类,面向OC业务层,提供了注册OC方法、调用JS方法等接口。
  • WebViewJavascriptBridge:基于UIWebView的的OC端交互逻辑处理类,与WKWebViewJavascriptBridge的功能一致。

WebViewJavascriptBridge源码解析

WebViewJavascriptBridge的实现可以说是双向的过程,无论是JS端还是Native端都包含以下三部分内容:

  • bridge初始化
  • 本端注册函数共另一端调用
  • 调用另一端函数

目前App已经取消对UIWebView的支持,所以我们只需要看WKWebView相关部分的实现即可。

bridge初始化

bridge初始化分为Native初始化bridge和JS初始化bridge。在使用WebView的时候,都是从Native端打开页面开始,因此先分析Native初始化bridge。

Native初始化bridge

+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _setupInstance:webView];
    [bridge reset];
    
    return bridge;
}

- (void)_setupInstance:(WKWebView*)webView {
    _webView = webView;
    // 将webView的navigationDelegate设为WKWebViewJavascriptBridge对象自身
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    // WKWebViewJavascriptBridge对象需要实现_evaluateJavascript:代理方法
    _base.delegate = self;
}

- (void)reset {
    [_base reset];
}

WKWebViewJavascriptBridge_evaluateJavascript:方法的实现:

- (NSString*)_evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

关于JS代码的注入,可以使用WKUserContentController,也可以使用evaluateJavaScript:completionHandler:这个函数,WebViewJavascriptBridge使用后者实现JS注入,因此需要将webView的navigationDelegate设为WKWebViewJavascriptBridge对象自身,并在代理方法中调用evaluateJavaScript:completionHandler:函数。

在Native初始化后,就要使用load之类的方法加载页面与JS代码,这进入到JS初始化bridge过程。

JS初始化bridge

相对于Native初始化bridge来说,JS初始化bridge就要显得难一些。


在JS初始化bridge过程中,会直接调用setupWebViewJavascriptBridge(callback)函数,callback相当于block/闭包。在这个过程中,callHandlerregisterHandler等函数都是通过JS代码注入的,这些代码都在Native端,那它是如何成功执行这些函数的呢?关键在于https://__bridge_loaded__这个url。在使用了这个url后,WKWebView的NavigationDelegate会拦截这个请求,并注入JS代码。相关实现如下:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    // ...
    // isBridgeLoadedURL函数中的kBridgeLoaded即为__bridge_loaded__
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    }
    // ...
}

用泳道图来描述初始化bridge的过程


WebViewJavascriptBridge源码分析_第1张图片
WebViewJavascriptBridge初始化bridge

JS注册函数,Native调用JS

JS注册函数

// JS注册函数给Native调用
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
    var responseData = { 'Javascript Says':'Right back atcha!' }
    responseCallback(responseData)
})

// WebViewJavascriptBridge_JS.m中的JS代码
// 保存JS函数与函数名的映射关系
var messageHandlers = {};
    
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

Native调用JS函数

id data = @{ @"greetingFromObjC": @"Hi there, JS!" };
[_bridge callHandler:@"testJavascriptHandler" data:data responseCallback:^(id response) {
    NSLog(@"testJavascriptHandler responded: %@", response);
}];

内部调用了WebViewJavascriptBridgeBasesendData:responseCallback:handlerName:方法。

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    // JS函数所需的参数
    if (data) message[@"data"] = data;
    // responseCallback:Native调用JS函数后的回调函数
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        // 保存Native回调
        self.responseCallbacks[callbackId] = [responseCallback copy];
        // 保存回调方法的id
        message[@"callbackId"] = callbackId;
    }
    // JS函数名
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

在Native调用JS的函数时,有时Native需要JS调用Native的回调函数返回一些数据,因此需要保存回调函数的一些信息,关于Native的回调函数是如何调用的,在后面会讲到。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

在开发过程中,有可能Native调用JS函数的时候,JS端还没有完成bridge准备工作。bridge是在decidePolicyForNavigationAction:的代理方法中执行injectJavascriptFile方法才完成的,但是
callHandler可能在viewWillAppear的时候调用,此时没有完成JS端bridge的初始化,所以先存入startupMessageQueue中,等准备完成后, 再统一调用 startupMessageQueue中的Message到JS,并将startupMessageQueue置为空。

- (void)injectJavascriptFile {
    // 注入JS bridge的环境代码
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    // 对于一些提前调用的callHandler,在注入JS初始化代码后,会统一发送,并清空startupMessageQueue
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
- (void)_dispatchMessage:(WVJBMessage*)message {
    // 序列化message
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    // 省略对messageJSON的处理
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];
    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}

_dispatchMessage将之前拼装好的message传给JS,用JS bridge的 _handleMessageFromObjC函数处理Native的调用请求,_handleMessageFromObjC函数是在JS bridge初始化的时候注入的。

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
    // 忽略dispatchMessagesWithTimeoutSafety部分
    _doDispatchMessageFromObjC();

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        // 是否有responseId,对于Native调用JS函数所传过来的message来说是没有该字段的
        // if (message.responseId) {
        // // ...
        // } 
        
        // 处理Native调用JS函数的message
        if (message.callbackId) {
            // 是否含有Native回调
            var callbackResponseId = message.callbackId;
            responseCallback = function (responseData) {
                // _doSend函数只传了message,另外没有responseCallback参数
                _doSend({
                    handlerName: message.handlerName,
                    // 如果Native传过来的message有回调,那么JS端需要传入一个responseId,这样Native端才能通过responseId这个key
                    // 在responseCallbacks字典中找到对应的Native回调
                    responseId: callbackResponseId,
                    responseData: responseData
                });
            };
        }

        // 通过handlerName获取到对应的JS函数,并调用。
        // messageHandlers保存了JS bridge的函数名和回调函数
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            handler(message.data, responseCallback);
        }
    }
}

// 这个_doSend是精简之后的实现,_doDispatchMessageFromObjC中的_doSend函数没有传递responseCallback参数
function _doSend(message) {
    // sendMessageQueue保存message信息,这个message信息是给Native回调时候用的
    sendMessageQueue.push(message);
    // src = https://__wvjb_queue_message__,WKWebView的代理方法优惠拦截这个url,从而调用WKWebViewJavascriptBridge的KFlushMessageQueue方法
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

当WKWebView的代理方法拦截到https://__wvjb_queue_message__这个url的时候,就会调用WKFlushMessageQueue方法

if ([_base isQueueMessageURL:url]) {
    [self WKFlushMessageQueue];
}

- (void)WKFlushMessageQueue {
    [_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(NSString* result, NSError* error) {
        NSLog(@"%@", result);
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

执行_fetchQueue()这个JS函数,并在completionHandler这个block内返回JS中sendMessageQueue的信息,从而获取带responseId的message。接着执行Native的flushMessageQueue方法。

// 在flushMessageQueue方法中完成了Native调用JS函数后的回调
- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判断
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略对Message类型的校验
        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        }
    }
}

用泳道图来描述Native调用JS的过程


WebViewJavascriptBridge源码分析_第2张图片
WebViewJavascriptBridgeNative调用JS

Native注册函数,JS调用Native

Native注册函数

[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"testObjcCallback called: %@", data);
    //
    responseCallback(@"Response from testObjcCallback");
}];

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    // messageHandlers用来保存OC函数与函数名的映射关系
    _base.messageHandlers[handlerName] = [handler copy];
}

JS调用Native函数

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

// JS端callHandler的实现
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

在Native调用JS的过程也使用了_doSend函数,它的作用是为了能调用Native调用JS函数之后的回调函数。在JS调用Native的过程中,_doSend函数是为了调用OC函数(与函数名对应的block),responseCallback则是代表JS调用OC函数后的回调函数。

function _doSend(message, responseCallback) {
    // 如果有JS回调,则使用responseCallbacks保存JS回调
    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;
}

之后的逻辑在OC调用JS中已经描述过了,直到执行flushMessageQueue:方法前都是一样的
这里就不再重复。接着看一下flushMessageQueue:方法在JS调用Native过程中的实现:

- (void)flushMessageQueue:(NSString *)messageQueueString{
    // 省略messageQueueString的有效性判断
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // 省略对Message类型的校验
        // 忽略关于responseId的实现
        // 对于JS调用Native所传过来的message来说是没有responseId字段的
        WVJBResponseCallback responseCallback = NULL;
        NSString* callbackId = message[@"callbackId"];
        // 判断是否有JS回调
        if (callbackId) {
            responseCallback = ^(id responseData) {
                if (responseData == nil) {
                    responseData = [NSNull null];
                }
                
                WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                // _queueMessage: -> _dispatchMessage: -> JS: _handleMessageFromObjC -> JS: _dispatchMessageFromObjC
                [self _queueMessage:msg];
            };
        } else {
            responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        }
        // 通过handlerName获取到对应的Native函数,并调用。
        // messageHandlers保存了Native bridge的函数名和回调函数
        WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
        
        if (!handler) {
            NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
            continue;
        }
        
        // 执行Native函数
        handler(message[@"data"], responseCallback);
    }
}

关于_queueMessage:方法前面已经分析过,这里就不再重复,接着看一下_dispatchMessageFromObjC函数在JS调用Native过程中的实现:

// 精简了_doDispatchMessageFromObjC,只保留调用JS回调的部分
function _doDispatchMessageFromObjC() {
    var message = JSON.parse(messageJSON);
    var responseCallback;
    // 如果有JS回调,那么OC传过来的message必然存在responseId字段
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        // 调用JS回调
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    } 
}

接着用泳道图来描述下JS调用Native的过程


WebViewJavascriptBridge源码分析_第3张图片
WebViewJavascriptBridgeJS调用Native

NNWKWebViewJSBridge

NNWKWebViewJSBridge是我在了解WebViewJavascriptBridge的实现过程后,基于这个项目,实现一个轻量级Swift版本JSBridge,并且它仅需要支持WKWebView即可。
相对于WebViewJavascriptBridge,我使用了WKUserContentController简化了初始化和消息传递的实现过程,相对来说会更好理解,消息传递性能也要比拦截Requests的方式要高。

项目地址:NNWKWebViewJSBridge
项目截图:

WebViewJavascriptBridge源码分析_第4张图片
WKWebViewJSBridge_demo

你可能感兴趣的:(WebViewJavascriptBridge源码分析)