js相关的代码分析

WebViewJavascriptBridge 是iOS开发中常用的类库,用于前端和手机端交互。

这里简单分析其源码,温故而知新。

前端调用代码

代码示例见链接

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    var WVJBIframe = document.createElement('iframe');
    WVJBIframe.style.display = 'none';
    WVJBIframe.src = 'https://__bridge_loaded__';
    document.documentElement.appendChild(WVJBIframe);
    setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

setupWebViewJavascriptBridge(function(bridge) {
    var uniqueId = 1
    bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
        log('ObjC called testJavascriptHandler with', data)
        var responseData = { 'Javascript Says':'Right back atcha!' }
        log('JS responding with', responseData)
        responseCallback(responseData)
    })
    bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
        log('JS got response', response)
    })
})

前端需要引入上述代码,setupWebViewJavascriptBridge(...)会执行以下操作:

  1. 会判断window.WebViewJavascriptBridge是否存在
  2. 如果存在则直接作为参数回调给function
  3. 如果不存在:
    • 将回调function存储到window.WVJBCallbacks
    • 在dom上添加iframe,并且约定地址为https://__bridge_loaded__
    • 原生在webview的代理方法中监测到该地址的访问时,会拦截该请求,同时注入js代码。执行注入代码时会对WVJBCallbacks进行遍历。

WebViewJavascriptBridge_JS.m 代码分析

WebViewJavascriptBridge_JS.m中首先定义了一个宏:

#define __wvjb_js_func__(x) #x

宏中包含了一个#,这种写法专业点叫Stringize, 作用是将宏转为字符串常量。例如:

#define toString(size) #size
printf(toString("good"));
NSLog(@toString("good"));

使用时虽然定义了很多行,其实字符串是在一行里。因此js代码开头和每个语句结尾都需要; , 用来防止其他行代码的影响。

此文件的js代码主要是对window.WebViewJavascriptBridge进行初始化,以及一些消息逻辑处理。 下面是删减过的一些代码:

window.WebViewJavascriptBridge = {
     //绑定前端注册消息的方法
    registerHandler: registerHandler,
    //绑定前端主动调用app的方法
    callHandler: callHandler,
    //`dispatchMessagesWithTimeoutSafety`是否禁用alertbox以加速消息的执行
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    //原生端通过此方法获取所有要执行的消息
    _fetchQueue: _fetchQueue,
    //原生端主动调用h5时,会触发此方法
    _handleMessageFromObjC: _handleMessageFromObjC
};
// 存储app需要执行的消息数组,调用`_doSend`方法时,会在数组中插入一条数据
var sendMessageQueue = [];
// 储存前端注册监听方法。`registerHandler`时会将监听方法进行存储
var messageHandlers = {};
// h5主动去调用原生时,存储一个callbacks,根据以自定义的callbackId作为key。用于处理h5调用原生,然后接收原生返回数据的后的匹配。
var responseCallbacks = {};

需要注意的是alert/prompt/confirm会阻塞消息的执行,因此这里引入了disableJavscriptAlertBoxSafetyTimeout来控制,但如果启用的话,可能影响安全性。

下面看下查看核心的两个方法:

_doSend: h5调用原生或者原生调用h5,都会触发此方法。 responseCallback只有在h5调用原生时才有值(h5调用原生才需要回调;h5接收到原生消息只需要将处理后的消息返回给原生,不需要callback)。

function _doSend(message, responseCallback) {
    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;
}

_dispatchMessageFromObjC: 处理原生发来的请求。

function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
        _doDispatchMessageFromObjC();
    }
    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        //不为空时,为h5调用原生,然后原生的消息返回
        if (message.responseId) {
            //根据消息id,找到h5注册的方法,并进行回调
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            //js主动调用原生的回调函数,只让他执行一次即可,不需要长久保存
            delete responseCallbacks[message.responseId];
        } else {
            //此处的代码为原生调用h5的场景            
            if (message.callbackId) { //原生如果需要h5的处理结果进行回调,则会传递过来callbackId
                var callbackResponseId = message.callbackId;  
                //定义h5处理结果返回原生的动作           
                responseCallback = function(responseData)
                {
                    _doSend({handlerName:message.handlerName, responseId:callbackResponseId,responseData:responseData});
                };
            }
            //若h5对方法进行了注册`registerHandler`,那么这里可以获取到
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //对h5注册方法进行回调
                handler(message.data, responseCallback);
            }
        }
    }
}

原生代码段

原生代码的处理方法总体来说和h5的处理逻辑差不多。

消息拦截

处理的核心就是对自定义消息的拦截:

  • 当触发__bridge_loaded__请求时,注入WebViewJavascriptBridge_JS.m中的js代码
  • 当触发__wvjb_queue_message__请求时,获取消息数组(WKFlushMessageQueue:),并且依次执行消息(flushMessageQueue:)
- (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;
    //思路是拦截iframe的请求
    // 判断是否自己内部定义的url,如__bridge_loaded__ 、__wvjb_queue_message__
    if ([_base isWebViewJavascriptBridgeURL:url]) {        
        if ([_base isBridgeLoadedURL:url]) {
            // 判断 __bridge_loaded__ 时,注入js。 __bridge_loaded__是前端手动注入的方法。 此时初始化桥js
            [_base injectJavascriptFile];            
        } else if ([_base isQueueMessageURL:url]) {
            //当注入的方法执行`_doSend`时,触发 __wvjb_queue_message__
            [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);
    }
}

消息执行

原生调用js就比较简单了,核心代码见flushMessageQueue:,此处执行的消息可能有两种类型:一种是原生调用h5,另一种是h5调用原生后的结果的回调。

- (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"];
        if (responseId) {
            // responseId不为空,说明是原生调用h5的回调的响应。
            //直接回调block闭包。 原生调用h5时,会将block存储到中,此处用到就取出来
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            WVJBResponseCallback responseCallback = NULL;
            NSString *callbackId = message[@"callbackId"];
            if (callbackId) {
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    // 闭包触发后,将数据返回给h5
                    WVJBMessage *msg = @{@"responseId": callbackId, @"responseData": responseData};
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            //因为是原生调用h5,因此对应的方法原生应该有注册,并保存到了messageHandlers这里。
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            //触发闭包响应
            handler(message[@"data"], responseCallback);
        }
    }
}

总结

整体来说jsbridge的代码比较清晰优雅的,很多细节值的学习。

Go语言学习笔记(三)