【iOS 开发】WebViewJavaScriptBridge 实现原理

时间:2022-06-09
本文章向大家介绍【iOS 开发】WebViewJavaScriptBridge 实现原理,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

WebViewJavaScriptBridge 使用一个不可见的 iFrame 来触发消息传递, 应该是为了兼容 UIWebView 和 WKWebView。

几个参数

  • kBridgeLoaded "bridge_loaded" 这个是加载 WebViewJavaScriptBridge 的消息,在第一次加载的时候原生会收到这个消息.收到这个消息后,原生把 JS 代码注入到 WebView 中。
  • kQueueHasMessage "wvjb_queue_message" 这个是消息发送的 url,收到这个消息后,调用 WKFlushMessageQueue 方法,开始走 JS调用原生流程.

JS 调用原生(以 WKWebView 为例)

  • callHandler => _doSend 设置 iFrame 的 src 属性
  • WKWebView 代理方法- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 收到事件
  • 通过 URL判断消息类型,如果是加载WebViewJavaScriptBridge 的消息,执行injectJavascriptFile方法,把 WebViewJavascriptBridge_JS 里面的代码注入到 WebView 中
  • 如果是客户端注册的消息,执行方法WKFlushMessageQueue, 此方法会执行 WebViewJavascriptBridge._fetchQueue(); 返回 JS 发送的消息对象数组
  • 拿到消息对象数组后,执行 - (void)flushMessageQueue:(NSString *)messageQueueString 解析消息
  • flushMessageQueue 方法通过 responseId 是否为空来判断些消息是否为原生调 js 的回调消息。
  • 如果是回调消息,直接通过 responseId 拿到回调的 block,拿到消息里面的 data,执行 block.
  • 如果是 js 调用原生的消息。先判断是否有 callbackId,如果有就生成一个回调的 block,如果没有就直接通过消息里面的 handlerName 拿到本地注册的 handlerBlock,执行 block。

主要两个方法注释如下:

/*
 这个方法主要是做消息解析
 */

- (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"];

        /*
         如果 responseId 存在,说明是原生调 js 方法的回调消息
         */

        if (responseId) {

            // 通过 responseId 拿到回调的 callback,执行回调
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];

        } else {

            /*
             没有 responseId ,说明是 js 调用原生的方法
             */

            WVJBResponseCallback responseCallback = NULL;
            // 拿出消息里面的回调 id
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {

                /*
                 如果回调 id 存在,说明 js 需要回调,生成一个回调block
                 */
                responseCallback = ^(id responseData) {

                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }

                    // 客户端处理完消息执行 callback 后,将数据跟 callbackId 组装成一个对象,给 JS 发送回调消息.
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _queueMessage:msg];
                };

            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }

            // 拿到客户端注册的对应消息的 block
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }

            // 执行消息
            handler(message[@"data"], responseCallback);
        }
    }
}
/*
 收到加载 WebViewJavaScriptBridge 的消息后,将 WebViewJavaScriptBridge_JS 里面的 js 代码注入到 js 中。
 */

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

原生调用 js

原生调用 js 就简单了,直接把参数组装成字符串,调用WebViewJavascriptBridge._handleMessageFromObjC('%@');

/*
 向 js 发送消息
 */

- (void)_queueMessage:(WVJBMessage*)message {

    /*
     startupMessageQueue 是在 js 代码还没有注入到 webView 里面时,用来保存消息的,注入代码后会执行这个队列里面的全部消息
     */
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}
/*
 组装消息,通过调用`WebViewJavascriptBridge._handleMessageFromObjC('%@');`发送到 js
 这个消息会在主线程调用
 */
- (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];
        });
    }
}

添加了部分中文注释的 WebViewJavascriptBridge