WebViewJavascriptBridge 源码解析

August 19, 2018   

项目用了 WebViewJavascriptBridge 这个强大的库来实现 JS 和 Swift 交互,已达到原生页面可以和网页进行数据的通讯。前段时间仔细研究了一下它的实现原理,窥探一下这个在 github 上拥有 11K stars 的库的厉害之处,看明白后发现原理其实也很简单,但他的实现思路可谓非常精妙!

其实,JS 和 原生语言进行通信的核心就是通过创建一个隐藏的 iframe 对象,改变其 src 实现加载请求,在webview中监听该请求和 用 UIWebView- (void) stringByEvaluatingJavaScriptFromString: 函数执行 JS 代码来配合实现数据的来回传递。

目前项目中使用的是 4.x 版本,而最新的版本已经更新到 6.x。

该库一共4个文件:由一个基类、供iOS使用的 WebViewJavascriptBridge 、一个供MacOS使用的 WKWebViewJavascriptBridge 和 JS 函数文件组成。

使用

先来看看怎么使用

  • 首先初始化 bridge 对象
// swift <-> javascript
fileprivate var bridge: WebViewJavascriptBridge!

...

override func viewDidLoad() {
  	bridge = WebViewJavascriptBridge(for: webView, delegate: webViewProgress)
}
  • 注册交互 handler
bridge.registerHandler("appFunc_IsHideNavBar") { (data, responseCallback) in
	// some logic here
}

这里说明下:

data: 从 JS 传递过来的参数,它是一个 id 类型,对应 js 中的 object

responseCallback: 回调函数,js 匿名函数

  • 调用一个方法,此时 js 中与该方法有相同签名的函数会被调用
bridge.callHandler("jsFunc_ReceiveParam", data: someParams)

调用时同样可以传递两种参数

data: 一个 id 类型的对象

handler: 回调函数(上例中省略)

在 web 项目中对应的交互 js 文件

// 这是4.x 版本的使用方法,6.x版本的方式已经改变,详见 github readme.md
function connectWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) {
        callback(WebViewJavascriptBridge);
    } else {
        document.addEventListener('WebViewJavascriptBridgeReady', function() {
            callback(WebViewJavascriptBridge);}, false
        );
    }
}

connectWebViewJavascriptBridge(function(bridge) {
    bridge.init(function(message, responseCallback) {
    })
});

注册一个交互方法,从原生获取用户的 token 方法 (js主动调用原生函数)

/**
 * 主动获取token
 * @param  {[Function]} processFunc [进程回调函数]
 */
function callAppFunc_CacheToken(processFunc){
    connectWebViewJavascriptBridge(function(bridge) {
        bridge.callHandler('appFunc_CacheToken', processFunc);
    });
}

这样 callAppFunc_CacheToken() 函数被执行时,对应 App 中的方法就会被掉起。

两种语言方法能够交互的核心是

它们都注册了相同的方法签名,即上面注册时传递的字符串

源码分析

WebViewJavascriptBridgeBase 基类是一个普通的 NSObject 对象,它有几个关键的属性:

  • delegate // UIWebView,一旦有执行改变时就会通知代理
  • startupMessageQueue // 数组,保存了所有的注册方法
  • responseCallbacks // 字典,保存所有的回调执行函数, key 是注册方法时的函数签名
  • messageHandlers // 字典 保存 js 注册的方法, key 是注册方法时的函数签名

我们在来看一下和App交互的核心文件 WebViewJavascriptBridge , 首先它继承自 WebViewJavascriptBridgeBase

#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 还是 MacOS 来导入对应的头文件,和声明是 UIWebView 还是 WebView。初始化一个bridge时:

- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView; // 保存用户传递过来的 webiview
    _webView.delegate = self; // 自己成为 Webview 的代理,监听网页加载状态的改变
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self; // 自己成为基类的代理
}

初始化的过程 iOS 和 MacOS 几乎一样。

当加载网页时,webview会通过代理方法通知到 WebViewJavascriptBridge 实例

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

因为在 WebViewJavascriptBridgeBase.h 头文件中通过宏定义了一种区别于 http/https 的协议 #define kOldProtocolScheme @"wvjbscheme" ,所以判断当前的请求是自定义类型还是,正常的 http/https 请求,如果是正常请求就通知 webview 的代理,如果是自己请求,则进一步判断是否执行过该请求和是否注册了该请求的方法,否则在控制台中输出找不到该方法的 console 信息。

  • 该请求被执行过,则重新注册交互

    - (void)injectJavascriptFile {
        NSString *js = WebViewJavascriptBridge_js();
        [self _evaluateJavascript:js]; // 执行js 生成 WebViewJavascriptBridge 对象,后文中会继续讲到
        if (self.startupMessageQueue) {
            NSArray* queue = self.startupMessageQueue;
            self.startupMessageQueue = nil;
            for (id queuedMessage in queue) {
                [self _dispatchMessage:queuedMessage];
            }
        }
    }
    

    如果执行过则清空当前 startupMessageQueue 数组,根据注册的方法重新生成一遍。

    这个过程会把 所有 注册的方法和回调保存在一个数组中,然后序列化成字符串传递给 JS,JS在接到这个字符串后再反序列化成数组,详见 - (void)_dispatchMessage: 方法的处理过程,正是所谓的分发消息。

  • 如果该请求在已有的消息队列中,则执行 js 方法,获取所有 js 注册好的回调方法信息。

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

    首先执行 JS 中 _fetchQueue(); 方法获取所有 JS 保存的方法和回调函数,该数组已经被序列化成字符串,再交给 flushMessageQueue 函数完成处理

下面关键的核心过程来了

- (void)flushMessageQueue:(NSString *)messageQueueString {
    if (messageQueueString == nil || messageQueueString.length == 0) {
    	// 如果该字符串为空,则说明 js 中 没有注册任何可以交互的方法,直接 return
        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];
        
        // 根据 responseId 获取 方法的签名,即上面中注册时传递的字符串,OC 和 JS 都要一样,否则会找不到
        NSString* responseId = message[@"responseId"];
        if (responseId) {
        	// 在 _responseCallbacks 中 根据这个签名来找到 OC 注册方法时传递的回调函数,然后执行该回调函数,并将 JS 传递过来的 id 类型的对象传递进这个回调函数中
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            // 直接执行 OC 回调函数 
            responseCallback(message[@"responseData"]);
            // 执行完成,从字典中删除该回调函数
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
        	// 如果方法签名不存在,则检查回到函数的id是否存在,如果存在,则生成一个空的回到函数,并保存在 startupMessageQueue 数组中
            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 {
            	// 如果回调函数id 都不存在则什么都不做
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            // 根据方法签名找到 从 JS 中传递过来的 函数
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            // 执行 JS 函数,并把 数据 和 刚生成的 OC 回调函数传过去
            handler(message[@"data"], responseCallback);
        }
    }
}

responseId 存在时,是 JS 主动发起调用 OC 的方法;不存在是 OC 主动发起调用JS的方法。

我们在来看看 JS 中的实现原理

其实很简单,就是给 window 增加了一个 WebViewJavascriptBridge 对象

window.WebViewJavascriptBridge = {
  registerHandler: registerHandler,
  callHandler: callHandler,
  disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
  _fetchQueue: _fetchQueue,
  _handleMessageFromObjC: _handleMessageFromObjC
};

该对象有几个关键方法:

  • registerHandler:注册一个方法和 OC 交互
  • callHandler: 调用一个 OC 注册好的同签名函数
  • _fetchQueue: 获取需要传递个 OC 的 数组对象,里面保存的是一个个需要执行的函数和回调
  • _handleMessageFromObjC: 顾名思义,处理 OC调用 JS 方法时的传递过来的数据和方法

这个 WebViewJavascriptBridge 对象同样有几个关键属性

  • sendMessageQueue: 保存所有需要发送给 OC 的信息,该数组保存了一个个回调函数
  • messageHandlers:object 根据方法签名保存 JS 注册的回调函数
  • responseCallbacks: object 根据方法签名保存了 OC 传递过来的回调函数

OC会在特定时候分别执行 WebViewJavascriptBridge 对象的 _fetchQueue_handleMessageFromObjC 方法,分别获取 JS 需要传递给OC 的信息 和 执行从OC 传递过来函数还是执行 JS 注册的方法,将注册好的回调函数传递过去。

这里 JS 和 OC 之所以能交互,是因为 docment 对象 生成了一个新的 iframe ,并将它的display设置为 none , 每次改变 它的 src 属性来实现加载新请求的目的

messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;

至此,JS 和 OC 的交互核心就讲完了。


comments powered by Disqus