美文网首页iOS-UI基础技术
WebViewJavascriptBridge 实现分析

WebViewJavascriptBridge 实现分析

作者: jacinzhang | 来源:发表于2022-12-17 00:20 被阅读0次

    UIWebView 已被全面废弃,故本文只分析 WKWebView 实现。源码见 WebViewJavascriptBridge

    先来看下在 WKWebView 下,是怎么使用JSBridge 的。

        WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
        webView.navigationDelegate = self;
        [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" }];
    

    所以,看来 JSBridge 对象是 WebViewJavascriptBridge 实现的。

    WebViewJavascriptBridge.h宏定义如下

    #if defined __MAC_OS_X_VERSION_MAX_ALLOWED
        #define WVJB_PLATFORM_OSX
        #define WVJB_WEBVIEW_TYPE WebView
        #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WebViewJavascriptBridgeBaseDelegate>
        #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<WebViewJavascriptBridgeBaseDelegate, WebPolicyDelegate>
    #elif defined __IPHONE_OS_VERSION_MAX_ALLOWED
        #import <UIKit/UIWebView.h>
        #define WVJB_PLATFORM_IOS
        #define WVJB_WEBVIEW_TYPE UIWebView
        #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<UIWebViewDelegate>
        #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
    #endif
    

    所以,在 iOS 上,其本质是

    NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
    

    WKWebView 也是这个类,也遵守 UIWebViewDelegate? 是否有点奇怪?

    看其内部实现,就会发现,当系统支持 WebKit 时,并且用户传的是 WKWebView 时,实例化 WebViewJavascriptBridge 对象时,就直接以 WKWebView 进行了初始化。

    + (instancetype)bridge:(id)webView {
    #if defined supportsWKWebView
        if ([webView isKindOfClass:[WKWebView class]]) {
            return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
        }
    #endif
        if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
            WebViewJavascriptBridge* bridge = [[self alloc] init];
            [bridge _platformSpecificSetup:webView];
            return bridge;
        }
        [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
        return nil;
    }
    

    所以,接下来,我们就去看 WKWebViewJavascriptBridge
    实例化对象

    + (instancetype)bridgeForWebView:(WKWebView*)webView {
        WKWebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _setupInstance:webView];
        [bridge reset];
        return bridge;
    }
    
    - (void) _setupInstance:(WKWebView*)webView {
        _webView = webView;
        _webView.navigationDelegate = self;
        _base = [[WebViewJavascriptBridgeBase alloc] init];
        _base.delegate = self;
    }
    

    上述代码又实例化了一个很重要成员变量 WebViewJavascriptBridgeBase

    整个 JSBridege objc 代码的实现就集中在了 WKWebViewJavascriptBridgeWebViewJavascriptBridgeBase,此外还有一个 JS 代码的注入文件 WebViewJavascriptBridge_js,整个库的核心可以说就是这几个文件了。

    WebViewJavascriptBridge 加载

    jsbridge-load
    可以看到,WebViewJavascriptBridge 加载过程主要如下:
    1. h5 网页加载,内部添加了一个不展示的 iframe,其 src 属性设置为了 https://__bridge_loaded__
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    
    1. iframe src 加载时,其请求被 WKWebView WKNavigationDelegate 代理方法拦截
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 
    
    1. 内部判断为加载 WebViewJavascriptBridge 请求,执行注入 js 代码 WebViewJavascriptBridge_js,实现 JSBridge,挂载在 window 上。
        window.WebViewJavascriptBridge = {
            registerHandler: registerHandler,
            callHandler: callHandler,
            disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
            _fetchQueue: _fetchQueue,
            _handleMessageFromObjC: _handleMessageFromObjC
        };
    

    注入 js 同时,又添加了一个 iframe 元素,其 src 设置为了 https://__wvjb_queue_message__,注入的时候就会加载,然后被 WKNavigationDelegate 拦截。

    如果 load web 之前,就调用了 callHandler,则会先存储起来,先执行注入 WebViewJavascriptBridge_js,然后再去 sendData。

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

    native 调用 h5

    jsbridge-native-invoke-h5.png

    以 native 调用H5 testJavascriptHandler 方法为例, native 调用 h5 主要经历如下几个步骤:

    1. h5 首先注册 testJavascriptHandler
    2. native 发送的数据,被包装成 json 字符串作为参数,然后使用 WebViewJavascriptBridge._handleMessageFromObjC('%@'); 在 WKWebView 执行 js 代码。json 字符串包含:data callbackId handlerName 参数。同时将回调根据callbackId存储起来。(callbackId 使用 objc_cb_ + 自增 id)
    3. js 方法 _dispatchMessageFromObjC 来处理数据。如果有 callbackId,则会使用 _doSend 方法,将callbackId 转为 responseId ,加入 testJavascriptHandler 返回的 responseData , 然后将数据存储到 sendMessageQueue 对象中,然后加载 messagingIframe src,src 链接为 https://__wvjb_queue_message__
    4. WKNavigationDelegate 代理拦截到 messagingIframe 重新加载 信息,执行 js 代码 WebViewJavascriptBridge._fetchQueue(); 获取存在 sendMessageQueue 中数据。
    5. sendMessageQueue 中获取到 responseId,根据 responseId 取到存储在2 中存储在 responseCallbacks 的 callback handler,然后 native 调用 handler,传入 js 的 responseData

    h5 调用 native

    jsbridge-h5-invoke-native.png
    以 h5 调用 native testObjcCallback handler 为例:
    1. native 需要先注册 testObjcCallback handler
    2. h5 调用 handler testObjcCallback
    bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
      log('JS got response', response)
    })
    
    1. js _doSend() 方法,处理调用,将 handlerName data callbackId 包装成JSON 字典对象, 存到 sendMessageQueue 中,并存储回调(如有调用有回调),然后设置 messagingIframe,让其重新加载https://__wvjb_queue_message__ 。其中 callbackId 生成规则 'cb_'+(uniqueId++)+'_'+new Date().getTime()

    2. WKNavigationDelegate 代理拦截 https://__wvjb_queue_message__,处理调用。首先通过 WebViewJavascriptBridge._fetchQueue(); 获取 js 数据,即 sendMessageQueue 中数据。

    3. 根据 sendMessageQueue 中数据,是否有 callbackId。如果有 callbackId, 即在 native handler 中,传入 responseData,并将 callbackId 转为 responseId, 然后再次 WKWebView 执行 js 代码。

    4. h5 WebViewJavascriptBridge 中 通过 _handleMessageFromObjC 方法处理调用。拿到 responseDataresponseId, 根据 responseId,找到3中存储的回调,然后执行。

    相关文章

      网友评论

        本文标题:WebViewJavascriptBridge 实现分析

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