JSBridge的实现与项目中的封装思路(一)

iOS JSBridge

Posted by NKQ on November 1, 2021

在iOS应用中使用网页来展示内容已经成为越来越风靡的方式。优点有很多。

使用WebViewJavascriptBridge

从苹果推出 WKWebView 后,比较建议使用 WKWebView 展示网页,而我入门较晚,接触到的也都是 WKWebView 的项目,它提供了很多与网页交互的方法,都比较基础,在它上面封装一层很有必要,可以方便开发者使用。

被广泛使用的是 Github 上 1W+Star 的 WebViewJavascriptBridge,开发者可以通过对它进一步封装,以此实现在APP中对JS请求的分发响应,完成各种操作。

WebViewJavascriptBridge 的使用比较简单

1
2
3
4
5
6
7
[self.bridge registerHandler:@"ObjC Echo" handler:^(id data, WVJBResponseCallback responseCallback) {
    NSLog(@"ObjC Echo called with: %@", data);
    responseCallback(data);
}];
[self.bridge callHandler:@"JS Echo" data:nil responseCallback:^(id responseData) {
    NSLog(@"ObjC received response: %@", responseData);
}];
1
2
3
4
5
6
7
8
9
10
11
12
setupWebViewJavascriptBridge(function(bridge) {

    /* Initialize your app here */

    bridge.registerHandler('JS Echo', function(data, responseCallback) {
        console.log("JS Echo called with:", data)
        responseCallback(data)
    })
    bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
        console.log("JS received response:", responseData)
    })
})

一个注册一个调用就完成了原生与JS的交互

怎么做到的

WebViewJavascriptBridge的源码仅有8个文件

img

其中有两组”xxxJavaScriptBridge”文件,分别代表了 WKWebView 和 WebView 的文件,一个用于iOS的 WKWebView,一个用在macOS的 WebView 和iOS的 UIWebView,主要看 WKWebViewJavaScriptBridge 这一组。

在 WKWebViewJavaScriptBridge 中,包括了如下方法

1
2
3
4
5
6
7
8
- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)removeHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
- (void)reset;
- (void)setWebViewDelegate:(id)webViewDelegate;
- (void)disableJavscriptAlertBoxSafetyTimeout;

一些注册、删除、呼叫、重置handler的方法,此外有两个方法,一个是设置 WebViewDelegate 方法,另一个是disableJavscriptAlertBoxSafetyTimeout,关于disableJavscriptAlertBoxSafetyTimeout,作者有写

UNSAFE. Speed up bridge message passing by disabling the setTimeout safety check. It is only safe to disable this safety check if you do not call any of the javascript popup box functions (alert, confirm, and prompt). If you call any of these functions from the bridged javascript code, the app will hang.

此方法可以加速消息的传递,会让交互更加快速,但是并不安全,开发者将不能使用js的”alert, confirm, and prompt”方法,否则,程序会被挂起。

看一下WebViewDelegate的具体实现,发现 webViewDelegate 是一个符合 WKNavigationDelegate 协议的对象,因此它可以实现对请求做导航,分发请求。

往下翻翻 WKNavigationDelegate 被使用的位置,均是简单封装后直接调用 WKNavigationDelegate 的方法,除了这个方法之外

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }

    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

这时候就需要看另一个文件了,这个文件是 WebViewJavascriptBridgeBase,在这个文件中定义了 “isWebViewJavascriptBridgeURL” 这个方法,直接说结论

如果这个请求的URL是一个所谓的 WebViewJavaScriptBridgeURL,那么它的host属性必须与这两者之一吻合

1
2
#define kQueueHasMessage    @"__wvjb_queue_message__"
#define kBridgeLoaded       @"__bridge_loaded__"

同时url的scheme属性也必须与自定义的#define kProtocolScheme @"xxxx"相吻合

先把这几个属性放在一边,继续往下看

BridgeLoadedURL

与kBridgeLoaded属性吻合的URL

1
2
3
4
5
6
7
8
9
10
11
- (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];
        }
    }
}

在 WebViewJavascriptBridgeBase 中,injectJavascriptFile这个方法把 WebViewJavascriptBridge_js 注入到JS端之后,如果在startupMessageQueue中也存在内容,会一并执行,但是我们暂时不看WebViewJavascriptBridge_js中的代码,先专注于查看OC部分的其余实现。

QueueMessageURL

与kQueueHasMessage属性吻合的URL

1
2
3
4
5
6
7
8
- (void)WKFlushMessageQueue {
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

首先运行了如下的JS代码

1
WebViewJavascriptBridge._fetchQueue();

由于还没看js的部分,这个JS代码作用先不管 在执行完JS的回调中,执行了WebViewJavascriptBridgeBase的flushMessageQueue方法,此方法做了以下事情

  • 对回调的result判空
  • 序列化result
  • 遍历其中的message,并对每一个message进行了处理,详细说说

message的处理

有responseID

首先取responseID,取到就认为是调用JS后的回调,从responseCallbacks取出来做对应的处理

那么这个responseCallbacks是从哪来的?看看 callHandler

1
2
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; // 调用sendData方法
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; // 维护了responseCallbacks

在这个方法中,对 responseCallbacks 这个字典做了维护,保存了执行代码的回调以及responseID

没有responseID

取不到的话证明是正常的JS请求,这时候做如下的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 {
    responseCallback = ^(id ignoreResponseData) {
        // Do nothing
    };
}

WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

if (!handler) {
    NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
    continue;
}

handler(message[@"data"], responseCallback);

对于正常的请求,还需要继续判定是否存在callbackId,这个目的很明显了,是为了返回给JS代码的CallBack用的,将此callbackID作为responseID发送回JS

如果取到了callbackId,那么就把这个请求直接执行或者放到startupMessageQueue等待执行。

然后,从messageHandlers中取出handler,也就是开发者注册的Handlers中,进行对应操作的执行,到了这一步,已经完成了JS对原生代码的调用。

总结

这篇文章粗略展示了JS发送请求使用 WebViewJavascriptBridge 调用原生代码的流程,但是还有很多东西没有说到,大约把整个 WebViewJavascriptBridge 的实现说了三分之一,会继续写后面的部分。