Principle Analysis of WebView JavascriptBridge

Posted by giannis_athens on Sat, 13 Jul 2019 03:23:05 +0200

Basic Notes

Our project is an app where OC interacts heavily with javascript, and the part where OC interacts with JavaScript is in github address of WebView JavascriptBridge Based on the modification, WebView Javascript Bridge should be the most popular and successful implementation of OC-Web interaction. Recently, I looked at his implementation principle, and by the way, I also laid the foundation for future project expansion.
In order to simplify the explanation process, I neglected the implementation process of UIWebView and only parsed the implementation process of WKWebView.

We can call the JavaScript method in OC, but in turn we can't call the OC method in javascript. So the implementation process of WebView Javascript Bridge is to store a mutual calling information in the OC environment and the JavaScript environment respectively. Each call has an id and a callbackid to find the processing corresponding to the two environments. The following is my explanation of each class:

  • The files under the nouse folder are related to UIWebView, which we ignore for the time being. The basic principle is the same as WKWebView. WebView JavascriptBridge_JS.m contains javascript code. To facilitate understanding, I directly created a new WebView JavascriptBridge_JS.js file to replace it for later parsing.

  • The WebView JavascriptBridge_JS.js file is the bridge initialization and processing of the javascript environment, which receives the messages from OC to javascript and sends the messages from the javascript environment to oc.

  • WKWebView JavascriptBridge. m is mainly responsible for message processing in the OC environment, and sends messages from the OC environment to the javascript environment.

  • WebView Javascript BridgeBase. m mainly implements bridge initialization and processing in OC environment.

  • ExampleApp.html is mainly used to simulate the web side of production environment.

Initialization process

1. OC environment initialization

We start with the initialization of the OC environment.

//Initialize the bridge WKWebView Javascript Bridge of an OC environment and initialize it.
+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    //Call the following method
    [bridge _setupInstance:webView];
    [bridge reset];
    return bridge;
}
//Initialization
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

//MessageageHandlers is used to save the method registered in the OC environment. The key is the method name, and the value is the callback block corresponding to the method.
//Startup MessageQueue is used to save messages that need to be sent to the javascirpt environment in the real process.
//ResponsseCallbacks are used to save callback modules that OC calls to each other in the javascript environment. The callback for each call is determined by adding a timestamp to _uniqueId.
- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}

All information that interacts with JavaScript is stored in message handlers and response Callbacks. These two attributes record the information that the OC environment interacts with javascript.

2. OC Environmental Registration Method

Register an OC method OC to provide methods for JS calls to javascript calls, and save his callback implementation in messageHandlers.

[_bridge registerHandler:@"OC Provide methods to JS call" handler:^(id data, WVJBResponseCallback responseCallback) {
    //NSLog(@"testObjcCallback called: %@", data);
    responseCallback(@"OC Issue JS The return value of");
}];

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

3. Web environment initialization

Load the html of the Web environment. Here is the ExampleAPP.html file. I deleted the non-critical parts.

function setupWebViewJavascriptBridge(callback) {
     //The first time this method is called, it is false
    if (window.WebViewJavascriptBridge) {
        var result = callback(WebViewJavascriptBridge);
        return result;
    }
    //The first call was also false
    if (window.WVJBCallbacks) {
        var result = window.WVJBCallbacks.push(callback);
        return result;
    }
    //Assign the callback object to the object.
    window.WVJBCallbacks = [callback];
    //This code means to execute the function of loading code in WebView JavascriptBridge_JS.js
    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);
}

//SetupWebView JavascriptBridge executes with the parameters passed in, which is a method.
function callback(bridge) {
    var uniqueId = 1
    //Register the method to be registered in WEB into bridge
    bridge.registerHandler('OC call JS The method provided', function(data, responseCallback) {
        log('OC call JS Successful method', data)
        var responseData = { 'JS to OC Callback of call':'Callback value!' }
        log('OC call JS The return value of', responseData)
        responseCallback(responseData)
    })
};
//Drive the initialization of all hander s
setupWebViewJavascriptBridge(callback);

We call the setupWebView JavascriptBridge function, and the callback passed in by this function is also a function. In the callback function, OC registered in the javascript environment calls the method and method provided by JS. During the implementation of setupWebView JavascriptBridge, we can find that if it is not the first initialization, it will be returned by two judgments: window. WebView JavascriptBridge or window.WVJBCallbacks.

Iframe can be understood as a window in webview, when we change the src attribute of iframe, it is equivalent to our browser to achieve a link jump. For example, jump from www.baidu.com to www.google.com. The purpose of the following code is to achieve a jump to https://_bridge_loaded_. So it can initialize the bridge of javascript environment.

//This code means to execute the function of loading code in WebView JavascriptBridge_JS.js
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);

We know that as long as the webview has a jump, the proxy method of the webview will be invoked. Let's focus on the following proxy method.

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    NSLog(@"Point open URL%@",url);
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    //If a message is sent or received by the WebView Javascript Bridge, it is handled specially. Otherwise, it should be handled according to the normal process.
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        //1 First injection of JS code
        if ([_base isBridgeLoadedURL:url]) {
            [_base injectJavascriptFile];
        //Processing messages from WEB
        } else if ([_base isQueueMessageURL:url]) {
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
    }
    //The following is the normal proxy execution process of webview, no matter what.
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

In this code, we first use [_base is WebView JavascriptBridgeURL: url] to determine whether it is a normal jump or a webView javascript Bridege jump. If _bridge_loaded_ means a message initializing the javascript environment, and if _wvjb_queue_message_ means sending a javascript message. Https://_bridge_loaded_ is obviously the first message. OC concrete judgment logic code is as follows:

#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__"

//Is it a link related to the WebView JavascriptBridge framework?
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url {
    if (![self isSchemeMatch:url]) {
        return NO;
    }
    BOOL result =  [self isBridgeLoadedURL:url] || [self isQueueMessageURL:url];
    return result;
}
/*
    Is it a message sent or received by WebView Javascript Bridge?
 */
- (BOOL)isSchemeMatch:(NSURL*)url {
    NSString* scheme = url.scheme.lowercaseString;
    BOOL result = [scheme isEqualToString:kNewProtocolScheme] || [scheme isEqualToString:kOldProtocolScheme];
    return result;
}
//Is it the message sent by the WebView Javascript Bridge or the initialization message of the WebView Javascript Bridge?
- (BOOL)isQueueMessageURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    return [self isSchemeMatch:url] && [host isEqualToString:kQueueHasMessage];
}
//Is it https://_bridge_loaded_ this initial load message?
- (BOOL)isBridgeLoadedURL:(NSURL*)url {
    NSString* host = url.host.lowercaseString;
    BOOL result = [self isSchemeMatch:url] && [host isEqualToString:kBridgeLoaded];
    return result;
}

Next, we call the [_base inject JavascriptFile] method, which injects the method in webview JavascriptBridge_JS.js into webview and executes it to initialize the brige of the javascript environment.

//Initialized inject WebView JavascriptBridge_JS.js
- (void)injectJavascriptFile {
    NSString *js;
    //The content of the WebView JavascriptBridge_JS.js file is actually the corresponding content of WebView JavascriptBridge_JS.m. I just sort it out for easy reading.
    if (true) {
        js = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"WebViewJavascriptBridge_JS.js" ofType:nil] encoding:NSUTF8StringEncoding error:nil];
    }else{
        js = WebViewJavascriptBridge_js();
    }
    //Inject javascript code into webview for execution, where specific injection operations are performed.
    [self _evaluateJavascript:js];
    //If the javascript environment is initialized, there is a startup MessageQueue message. Send the message immediately.
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}
//Write javascript code into webview
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand {
    [_webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    return NULL;
}

3. WebView JavascriptBridge_JS.js parsing

Above we talked about injecting javascript methods into webview. The specific code is the method in the file WebView JavascriptBridge_JS.js. By analyzing the code of this file, we can see how the bridge of the javascript environment is initialized.

;(function() {
    //If it has been initialized, it returns.
    if (window.WebViewJavascriptBridge) {
        return;
    }
    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    //Initialize some properties.
    var messagingIframe;
    //Used to store message lists
    var sendMessageQueue = [];
    //Used to store messages
    var messageHandlers = {};
    //A combination of the following two protocols is used to determine whether a particular message is present, and then to intercept it.
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    //Callback of oc calling js
    var responseCallbacks = {};
    //id corresponding to message
    var uniqueId = 1;
    //Whether to set message timeout
    var dispatchMessagesWithTimeoutSafety = true;
    //Register a message method on the web side
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    //web-side calls an OC registered message
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName: handlerName, data: data }, responseCallback);
    }
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
        //Converting messages to JSON strings and returning them
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    //OC Input Method to Call JS
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

    //Initialize the bridge object, OC can call various methods in JS through WebView Javascript Bridge.
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };


    //Processing messages returned from OC.
    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
            _doDispatchMessageFromObjC();
        }

        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;
            //Callback
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {//Active call
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                    };
                }
                //Getting JS registered functions
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //Calling the corresponding function processing in JS
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    //Send the message from JS to OC to perform the specific sending operation.
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            //Callback ID for storing messages
            responseCallbacks[callbackId] = responseCallback;
            //Send the corresponding callback ID of the message together with the message for later use when the message returns.
            message['callbackId'] = callbackId;
        }
        //Put the message in the message list
        sendMessageQueue.push(message);
        //The following sentence will start JS's call to OC
        //Let the webview perform a jump operation so that it can
        //WebView:(WK WebView*) webView Decision Policy for Navigation Action:(WK Navigation Action*) Navigation Action Decision Handler:(void (^) (WK Navigation Action Policy)) Decision Handler intercepts JS messages sent to OC
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    //messagingIframe.body.style.backgroundColor="#0000ff";
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);


    //Register the _disableJavascriptAlertBoxSafetyTimeout method so that OC can turn off the callback timeout, which is turned on by default.
    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    //Execute the _callWVJBCallbacks method
    setTimeout(_callWVJBCallbacks, 0);

    //Initialize the method of registration in WEB. This method registers hander s in WEB into bridge s.
    //The following code actually executes the callback function in WEB.
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();

In fact, we found that the entire js file is a javascript method that executes immediately.

  • First we find that a WebView JavascriptBridge object is initialized. And this object is assigned to the window object, which can be understood as webview. So if we want to call js method in OC environment later, we can use window. WebView JavascriptBridge to add specific method to call.

  • The WebView javascript Bridge object has a method registerHandler injected by the javascript environment to provide OC calls, and javascript calls the callHandler of the OC environment method.

  • _ The function of fetchQueue method is to serialize the method of javascript environment into JSON string, and then pass it into OC environment for conversion.

  • _ handleMessageFromObjC is the way to deal with OC sending to the javascript environment.

In this file, an iframe is also initialized to implement the url jump function of webview, which triggers the invocation of the WebView proxy method.

    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    //messagingIframe.body.style.backgroundColor="#0000ff";
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);

The src above is https://_wvjb_queue_message_/. This is the first message of OC sent by javascript for the same purpose as the startup MessageQueue of the above OC environment, which is to send the message that javascript wants to send to OC immediately after the initialization of the javascript environment is completed.

Then we can see the following code at the end of the file. The purpose of this code is to immediately execute the callback method in ExampleApp.html. The bridge parameter passed in the callback is the window. WebView JavascriptBridge object we initialized here.

    //Execute the _callWVJBCallbacks method
    setTimeout(_callWVJBCallbacks, 0);

    //Initialize the method of registration in WEB. This method registers hander s in WEB into bridge s.
    //The following code actually executes the callback function in WEB.
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }

Until now, bridege s for both OC and javascript environments have been established. OC and javascript environments have a bridge object, which holds every method and callback registered, and maintains a series of information such as message queue, callback id, requestId, etc.

OC sends messages to WEB

OC calls the method of javascript environment, which is actually the method of bridge.registerHandler registration in ExampleApp.html.

//Click on the button to start an OC message. ExampleWK WebViewController. M.
- (void)callHandler:(id)sender {
    id data = @{ @"OC call JS Method": @"OC call JS Parameters of the method" };
    [_bridge callHandler:@"OC call JS The method provided" data:data responseCallback:^(id response) {
       // NSLog(@"testJavascriptHandler responded: %@", response);
    }];
}
/*
    handerName:OC Call the method provided by JS
    data:{@"OC The parameter of calling JS method ":@" OC calls JS method "}
    responseCallback:Callback block
 */
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

Put all the information in a dictionary named message. The parameters data, callback IDcallbackId and message name handlerName are assembled. Specifically as follows:

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    
    if (data) {
        message[@"data"] = data;
    }
    
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];
        message[@"callbackId"] = callbackId;
    }
    
    if (handlerName) {
        message[@"handlerName"] = handlerName;
    }
    [self _queueMessage:message];
}

Serialize OC messages and convert them into the format of the javascript environment. Then call _evaluateJavascript in the main thread.

//Send messages to the WEB environment
- (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];
        });
    }
}

The specific injected javascript strings are as follows:

WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC call JS Method\":\"OC call JS Parameters of the method\"},\"handlerName\":\"OC call JS The method provided\"}');

In fact, it is through the _handleMessageFromObjC method of the Bridge object in the javascript environment. Let's go to WebView JavascriptBridege_JS.js to see the process of _handleMessageFromObjC.

//Processing messages returned from OC.
function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
        _doDispatchMessageFromObjC();
    }

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        //Callback
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {//Active call
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                responseCallback = function(responseData) {
                    _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                };
            }
            //Getting JS registered functions
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                //Calling the corresponding function processing in JS
                handler(message.data, responseCallback);
            }
        }
    }
}

The above code is easy to understand, in fact, if there is a callbackId in the message, it means a callback. Call the _doSend method directly to return the information to OC. Otherwise, OC is actively invoked by the Web environment. At this point, encapsulate the callbackID, handlerName, and responseCallback into a message object and save it (in fact, you will find that it is the same as the bridge processing in OC environment). The message is then sent to the OC environment via _doSend. Let's look at the implementation of _doSend:

//Send the message from JS to OC to perform the specific sending operation.
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //Callback ID for storing messages
        responseCallbacks[callbackId] = responseCallback;
        //Send the corresponding callback ID of the message together with the message for later use when the message returns.
        message['callbackId'] = callbackId;
    }
    //Put the message in the message list
    sendMessageQueue.push(message);
    //The following sentence will start JS's call to OC
    //Let the webview perform a jump operation so that it can
    //WebView:(WK WebView*) webView Decision Policy for Navigation Action:(WK Navigation Action*) Navigation Action Decision Handler:(void (^) (WK Navigation Action Policy)) Decision Handler intercepts JS messages sent to OC
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

Most importantly, the last one is to change the messagingIframe.src of iframe. This triggers the proxy method webview:(WK webview*) webview Decision Policy for Navigation Action:(WK Navigation Action*) Navigation Action Decision Handler:(void (^) (WK Navigation Action Policy)) to handle callbacks triggered by the javascript environment in OC. Specifically as follows:

if ([_base isWebViewJavascriptBridgeURL:url]) {
    //First injection of JS code
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    //Processing messages from WEB
    } else if ([_base isQueueMessageURL:url]) {
        [self WKFlushMessageQueue];
    } else {
        [_base logUnkownMessage:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
}

Here we go [self WKFlushMessageQueue]; method. Then the callback information from javascript to OC is obtained by calling WebView JavascriptBridge. _fetchQueue ().

//JSON String for Getting WEB Messages
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
//// Send messages or WEB callbacks from OC to OC
- (void)WKFlushMessageQueue {
    NSString *js = [_base webViewJavascriptFetchQueyCommand];
    [_webView evaluateJavaScript:js completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        //Send messages or WEB callbacks from OC to OC
        [_base flushMessageQueue:result];
    }];
}

After getting the callback message from javascript to OC, the information returned from the bridge of javascript is processed into the information recognized by the bridge of OC environment. In order to find specific implementation.

//Return the message sent from WEB. And then deal with it here.
- (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) {
            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];
                    }
                    
                    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);
        }
    }
}

The handler method is invoked here, and the corresponding WVJBResponseCallback is obtained by responseId passed over from javascript. Then execute the block. The process of sending messages from OC to JavaScript and returning messages from JavaScript to OC is over.

WEB sends messages to OC

First, through the bridge.callHandler method in ExampleAPP.html, the bridge here is the window. WebView JavascriptBridge object:

bridge.callHandler('OC Provide methods to JS call',params, function(response) {
    log('JS call OC The return value of', response)
})

Next, call the callHander method in window. WebView JavascriptBridge

//web-side calls an OC registered message
function callHandler(handlerName, data, responseCallback) {
    if (arguments.length == 2 && typeof data == 'function') {
        responseCallback = data;
        data = null;
    }
    _doSend({ handlerName: handlerName, data: data }, responseCallback);
}

Then call the method in WebView JavascriptBridge_JS.js to perform the specific operation. Specifically, as OC calls the javascript process, it is not explained.

//Send the message from JS to OC to perform the specific sending operation.
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        //Callback ID for storing messages
        responseCallbacks[callbackId] = responseCallback;
        //Send the corresponding callback ID of the message together with the message for later use when the message returns.
        message['callbackId'] = callbackId;
    }
    //Put the message in the message list
    sendMessageQueue.push(message);
    //The following sentence will start JS's call to OC
    //Let the webview perform a jump operation so that it can
    //WebView:(WK WebView*) webView Decision Policy for Navigation Action:(WK Navigation Action*) Navigation Action Decision Handler:(void (^) (WK Navigation Action Policy)) Decision Handler intercepts JS messages sent to OC
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

summary

In fact, if you think about it now, the principle is very simple.

  • In both OC and javascript environments, a bridge object is saved, which maintains requestId,callbackId, and the specific implementation of each id.

  • OC finds specific methods through the window. WebView JavascriptBridge object of the javascript environment and executes them.

  • javascript starts the proxy method of webview by changing the src of iframe, webview:(WKWebView*) webview decision policy for Navigation Action:(WKNavigation Action*) navigationAction decisionHandler:(void (^) (WKNavigation Action Policy)) to realize the function of sending javascript messages to OC.

In fact, this paper only analyses the bridge problem of interaction between WebView and OC. Other issues, such as request interception in webview, adding progress bar, operator hijacking, how to organize interaction rules, have not been addressed here. These are used in our project, and they will not be taken out.

Finally, the specific source code is github address.

Topics: Javascript JSON github Attribute