What is JSBridge?

JSBridge is a means of communication between the Webview side and the native side. The Webview can invoke native capabilities through JSB, and the native side can execute some logic on the Webview through JSB.

Ii. Implementation of JSB

In the popular JSBridge, the communication between native end and WebView end is achieved mainly by intercepting URL requests.

Here we take the popular WebviewJavascriptBridge as an example to parse its implementation.

Source code address: github.com/marcuswesti…

2-1. Register the Bridge at the Native end and webView end

During registration, the Bridge needs to be registered on the WebView side and the Native side respectively. In fact, it uses an object to store all functions.

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

2-2. Inject initialization code into webView

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)}Copy the code

This code does a few things:

(1) Create an array named WVJBCallbacks and place the passed callback arguments into the array

(2) Create an iframe, set it invisible, and set SRC to https://__bridge_loaded__

(3) Set the timer to remove the IFrame

2-3. Listen for URL requests at native end

There are two kinds of webView in iOS, one is UIWebview and the other is WKWebview. Here, WKWebview is used as an example:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^) (WKNavigationResponsePolicy))decisionHandler {
    if(webView ! = _webView) {return; }

    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) {
        [strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler];
    }
    else {
        decisionHandler(WKNavigationResponsePolicyAllow); }}Copy the code

This code does a few things:

(1) Block all URL requests and get the URL

(2) the first judgment isWebViewJavascriptBridgeURL, whether the url is webview iframe triggered, concrete can be host to judge.

(3) If isBridgeLoadedURL, injectJavascriptFile will be executed, and some logic will be injected into the WebView again. The most important logic is: Mount some global variables and WebViewJavascriptBridge properties on the window object with the following values:

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

var sendMessageQueue = [];
var messageHandlers = {};

var responseCallbacks = {};
var uniqueId = 1;
Copy the code

If it is isQueueMessageURL, then it is a callback that handles the message and needs to execute some message handling methods (more on this in Step 4)

2-4. Webview calls native ability

After both Native and WebView register the Bridge, they can call each other. Here is the process of webView invoking native capabilities.

2-4-1 webView callHandler

When webView calls Native, callHandler method will be called. The specific logic of this method is as follows:

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

function callHandler(handlerName, data, responseCallback) {
	       if (arguments.length == 2 && typeof data == 'function') {
		              responseCallback = data;
		              data = null;
	        }
	       _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

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;
}
Copy the code

In effect, create a message, push it into sendMessageQueue, and change the SRC of iframe.

2-4-2, native side flushMessageQueue

Then, when the native end detects the change of iframe SRC, it will go to the judgment logic of isQueueMessageURL and execute the WKFlushMessageQueue function. Get all messages in sendMessageQueue on JS side.

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

- (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:[WVJBMessageclass]]) {
            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); }}}Copy the code

When a message structure exists in the responseId, the message is returned after the bridge is performed. If bridge is not available, a message is returned to the responseId (reponseId is the callbackId of the message). When the native responseCallback is executed, the _dispatchMessage method is triggered to execute the JS logic of the WebView environment and return the generated message containing the responseId to the WebView.

2-4-3, WebView side handleMessageFromObjC

function _handleMessageFromObjC(messageJSON) {
    _dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
	      if (dispatchMessagesWithTimeoutSafety) {
		           setTimeout(_doDispatchMessageFromObjC);
	} else {
		           _doDispatchMessageFromObjC();
	}

	function _doDispatchMessageFromObjC() {
		      var message = JSON.parse(messageJSON);
		      var messageHandler;
		      var responseCallback;
		      if (message.responseId) {
			             responseCallback = responseCallbacks[message.responseId];
			             if(! responseCallback) {return;
			             }
			             responseCallback(message.responseData);
			             delete responseCallbacks[message.responseId];
		       } else {
			            if (message.callbackId) {
				                   var callbackResponseId = message.callbackId;
				                   responseCallback = function(responseData) {
					                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
				                    };
			             }

			             var handler = messageHandlers[message.handlerName];
			             if(! handler) {console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
			             } else{ handler(message.data, responseCallback); }}}}Copy the code

If there is responseId in the message obtained from Native, it indicates that this message is the message received by JS callback after calling Native. So take that callback from the responseCallbacks that we added in sendData at the beginning and execute it according to the responseId (the callbackId that was originally stored, the same value), In this way, a PROCESS of JS calling Native is completed.

2-4-4. Process summary

The process is shown as follows:

1. Register JSB at native end

2. Create an iframe on the webView side and set SRC to __bridge_load__

3. Native captures the request, injects JSB initialization code, and mounts related objects and methods on window

4, WebView side call callHandler method, add callbackId: responseCallback to responseCallback, modify iframe SRC, trigger capture

5. Native receives the message, generates a responseCallback, and executes the method registered by Native side

6. After native execution, execute _handleMessageFromObjC method through webView, take out callback function, and execute it

2-5. Native ability to call WebView

The logic of native calling the JSB registered with the WebView is similar, except that it is not executed through the SRC trigger that triggers the IFrame, since Native can actively call the JS side methods itself. The specific process is shown as follows:

1. Call callHandler (native) and add callbackId: responseCallback to responseCallback

2. The Native side actively calls the _handleMessageFromObjC method and executes the corresponding logic in webView

3. After the execution on the webView side is complete, generate a message with responseId, add it to sendMessageQueue, and change the SRC of iframe to __wvjb_queue_message__

4. The Native end intercepts the URL change, calls the webView logic to get the message, gets the responseId, and executes the corresponding callback function