preface

Emmmmm… This article may be published just in time for Christmas 🎄, Merry Christmas!

How JS in Web pages interact with iOS Native is a skill that every iOS ape must master. JS and iOS Native are just like two continents without intersection. If they want to communicate with each other, they must build a “bridge”.

Think about it. If the project team asked you to build the bridge, how would it be elegant and functional?

This article will combine WebViewJavascriptBridge source code step by step with you to find the answer.

WebViewJavascriptBridge is a well-known JSBridge library. As early as 2011, it was published on GitHub by Marcus Westin, and the author is still actively maintaining it until now. At present, the project has gained nearly 1w stars. Its source code is worth our study.

The code of WebViewJavascriptBridge has clear logic and good style, and the relatively small amount of code makes it very easy to read the source code (some JS foundation may be required). What is more valuable is that it only uses a small amount of code to achieve perfect support for Mac OS X WebView and iOS UIWebView and WKWebView components.

My evaluation of WebViewJavascriptBridge is small and beautiful, this kind of small and beautiful source is very conducive to our learning of its implementation ideas (this article analyzes the source version of WebViewJavascriptBridge for v6.0.3).

As for the knowledge of native interaction between iOS and JS, I wrote an article called “iOS and JS Interactive Development Knowledge Summary” before. In addition to the introduction of JavaScriptCore library and the methods of UIWebView and WKWebView to interact with NATIVE JS, the article also introduces the development history of Hybrid. At the end of this article, we also provide a Demo of JS calling the camera of iOS device through Native.

Therefore, this article will no longer focus on the native interaction between iOS and JS. This article aims to introduce the design ideas and implementation principles of WebViewJavascriptBridge. Students who are interested in the knowledge of native interaction between iOS and JS are recommended to read the article mentioned above. That should help a little bit [laughs].

The index

  • WebViewJavascriptBridge profile
  • WebViewJavascriptBridge && WKWebViewJavascriptBridge explored
  • WebViewJavascriptBridgeBase – JS call Native implementation principle
  • Webviewjavascriptbridge_js-native calls JS to implement interpretation
  • WebViewJavascriptBridge’s “bridge aesthetics”
  • The article summarizes

WebViewJavascriptBridge profile

WebView javascriptBridge is an iOS/OSX bridge for sending messages between OBJ-C and JavaScript in WKWebView, UIWebView and WebView.

There are a lot of great projects that use WebViewJavascriptBridge, here’s a quick list (laughs) :

  • Facebook Messenger
  • Facebook Paper
  • ELSEWHERE
  • . & many more!

For details on how to use WebViewJavascriptBridge, see its GitHub page.

After reading the source code of WebViewJavascriptBridge, I divided it into three levels:

The hierarchy The source file
The interface layer WebViewJavascriptBridge && WKWebViewJavascriptBridge
Implementation layer WebViewJavascriptBridgeBase
JS layers WebViewJavascriptBridge_JS

The WebViewJavascriptBridge && WKWebViewJavascriptBridge as interface layer is responsible for providing convenient interface, hide the implementation details, Its implementation details are by implementing layer WebViewJavascriptBridgeBase to do, but WebViewJavascriptBridge_JS as JS layers actually stores a JS code, in need of injection to the current WebView component, Finally realize the interaction between Native and JS.

WebViewJavascriptBridge && WKWebViewJavascriptBridge explored

WebViewJavascriptBridge and WKWebViewJavascriptBridge as interface layer correspond to the UIWebView and WKWebView components, let’s look at the simple information exposed by the two files:

WebViewJavascriptBridge exposes information:

@interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE

+ (instancetype)bridgeForWebView:(id)webView; / / initialization
+ (instancetype)bridge:(id)webView; / / initialization

+ (void)enableLogging; // Enable logging
+ (void)setLogMaxLength:(int)length; // Set the maximum log length

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler; // Register handler (Native)
- (void)removeHandler:(NSString*)handlerName; // Delete handler (Native)
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback; // Call handler (JS)
- (void)setWebViewDelegate:(id)webViewDelegate; / / set webViewDelegate
- (void)disableJavscriptAlertBoxSafetyTimeout; // Disable the JS AlertBox security duration to speed up message delivery. Not recommended

@end
Copy the code

WKWebViewJavascriptBridge exposure information:

// Emmmmm... I don't think I need to comment here
@interface WKWebViewJavascriptBridge : NSObject<WKNavigationDelegate.WebViewJavascriptBridgeBaseDelegate>

+ (instancetype)bridgeForWebView:(WKWebView*)webView;
+ (void)enableLogging;

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
- (void)removeHandler:(NSString*)handlerName;
- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
- (void)reset;
- (void)setWebViewDelegate:(id)webViewDelegate;
- (void)disableJavscriptAlertBoxSafetyTimeout;

@end
Copy the code

Note: disableJavscriptAlertBoxSafetyTimeout method is by disabling JS end AlertBox safe time to speed up the bridge of messaging. If you want to use this method, you need to agree with the front-end. If the front-end JS code still calls alert, confirm, or Prompt codes after disabling this method, the program will be suspended. Therefore, this method is not safe, and I do not recommend using it without special requirements.

You can see that the interfaces exposed by these two files are almost identical, The macro definition WVJB_WEBVIEW_DELEGATE_INTERFACE is used in WebViewJavascriptBridge to adapt UIWebView and WebView of iOS and Mac OS X respectively Proxy methods that the component needs to implement.

Macro definition in WebViewJavascriptBridge

In fact, WebView javascriptBridge in order to adapt iOS and Mac OS X platform UIWebView and WebView components use a series of macro definitions, the source is relatively simple:

#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
Copy the code

WVJB_WEBVIEW_TYPE, WVJB_WEBVIEW_DELEGATE_TYPE, and the WVJB_WEBVIEW_DELEGATE_INTERFACE macro definitions, depending on the platform, We define WVJB_PLATFORM_OSX and WVJB_PLATFORM_IOS, respectively, for later implementation source code to use on the current platform. The following supportsWKWebView macro definition does the same:

#if (__MAC_OS_X_VERSION_MAX_ALLOWED > __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_7_1)
#define supportsWKWebView
#endif
Copy the code

The supportsWKWebView macro gives you the flexibility to import the required headers when importing them:

// WebViewJavascriptBridge.h
#if defined supportsWKWebView
#import <WebKit/WebKit.h>
#endif

// WebViewJavascriptBridge.m
#if defined(supportsWKWebView)
#import "WKWebViewJavascriptBridge.h"
#endif
Copy the code

WebViewJavascriptBridge implementation analysis

Let’s look at the implementation of WebViewJavascriptBridge, starting with the internal variable information:

#if __has_feature(objc_arc_weak)
    #define WVJB_WEAK __weak
#else
    #define WVJB_WEAK __unsafe_unretained
#endif

@implementation WebViewJavascriptBridge {
    WVJB_WEAK WVJB_WEBVIEW_TYPE* _webView; // Bridge corresponding WebView component
    WVJB_WEAK id _webViewDelegate; // Set the proxy for the WebView component (if needed)
    long _uniqueId; // Emmmmm... But I found no eggs, only _uniqueId in _base
    WebViewJavascriptBridgeBase *_base; / / have said, the underlying implementation is WebViewJavascriptBridgeBase actually doing
}
Copy the code

Above WebViewJavascriptBridge and WKWebViewJavascriptBridge. H file exposed interface information is very similar, so we want to have a look at WKWebViewJavascriptBridge internal variable information?

// See WebViewJavascriptBridge for comments
@implementation WKWebViewJavascriptBridge {__weak WKWebView* _webView;
    __weak id<WKNavigationDelegate> _webViewDelegate;
    long _uniqueId;
    WebViewJavascriptBridgeBase *_base;
}
Copy the code

Well, they were born of the same mother. Actually, this is intentional, because the author wants to provide an interface, a WebViewJavascriptBridge, We just need to use WebView javascript Bridge to automatically generate the corresponding JSBridge instance based on the different WebView component that is bound.

+ (instancetype)bridge:(id)webView {
// If WKWebView is supported
#if defined supportsWKWebView
    // Check whether the current input webView belongs to WKWebView
    if ([webView isKindOfClass:[WKWebView class]]) {
        / / return WKWebViewJavascriptBridge instance
        return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
    }
#endif
    // Determine whether the current input parameter webView belongs to webView (Mac OS X) or UIWebView (iOS)
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        // Returns the WebViewJavascriptBridge instance
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    
    // Throws the BadWebViewType exception and returns nil
    [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
    return nil;
}
Copy the code

As we can see from the code above, the implementation is not complicated. If support WKWebView (# if defined supportsWKWebView) is to judge whether the current binding WebView component from WKWebView, so I can return WKWebViewJavascriptBridge instance, Otherwise, an instance of webView javascriptBridge is returned, and a BadWebViewType exception is raised if the type of the input webView does not meet the criteria.

There’s one more little detail about _webViewDelegate that I wasn’t going to talk about, but I’ll mention it anyway. Actually in WebViewJavascriptBridge and WKWebViewJavascriptBridge initializes the implementation process, the current WebView component of the proxy binding for yourself:

// WebViewJavascriptBridge
- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
    _webView = webView;
    _webView.delegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

// WKWebViewJavascriptBridge
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
Copy the code

Note: The proxy for the replacement component binds its proxy to bridge itself because WebViewJavascriptBridge is implemented in principle using the fake Request method described in my previous article “iOS and JS interactive development summary”. So you need to listen to the proxy method of the WebView component to get the request. URL before loading and do something about it. That’s why WebViewJavascriptBridge provides an interface called setWebViewDelegate: Stores a logical _webViewDelegate, which also follows the WebView component’s proxy protocol, So all you have to do to make a bridge in a different proxy method inside a WebViewJavascriptBridge is call the _webViewDelegate proxy method, Javascriptbridge hooks the current WebView component’s proxy.

Exposed in WebViewJavascriptBridge all interfaces, outside of the initialization of its internal implementation is done by WebViewJavascriptBridgeBase. Because the benefits are even WebViewJavascriptBridge binding WKWebView returned WKWebViewJavascriptBridge instance, as long as the interface is consistent, the JSBridge sending the same message, There will be the same implementation) by (WebViewJavascriptBridgeBase class implements.

WebViewJavascriptBridgeBase – JS call Native implementation principle

As WebViewJavascriptBridge implementation layer, WebViewJavascriptBridgeBase naming can also reflect the piers is spread as a “bridge” the existence of the general, We did as usual, looked WebViewJavascriptBridgeBase. H exposure information, good to have a overall impression:

typedef void (^WVJBResponseCallback)(id responseData); / / callback block
typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); // Register Handler block
typedef NSDictionary WVJBMessage; // Message type - dictionary

@protocol WebViewJavascriptBridgeBaseDelegate <NSObject>
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand;
@end

@interface WebViewJavascriptBridgeBase : NSObject

@property (weak.nonatomic) id <WebViewJavascriptBridgeBaseDelegate> delegate; // proxy, pointing to the interface layer class, used to send execution JS messages to the corresponding interface bound WebView component
@property (strong.nonatomic) NSMutableArray* startupMessageQueue; // Start the WVJBMessage queue
@property (strong.nonatomic) NSMutableDictionary* responseCallbacks; // call the blocks dictionary to store blocks of type WVJBResponseCallback
@property (strong.nonatomic) NSMutableDictionary* messageHandlers; Handlers dictionary, which holds blocks of type WVJBHandler
@property (strong.nonatomic) WVJBHandler messageHandler; / / not eggs

+ (void)enableLogging; // Enable logging
+ (void)setLogMaxLength:(int)length; // Set the maximum log length
- (void)reset; // The reset interface corresponds to WKJSBridge
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName; // Send a message with arguments, and call back the block corresponding to the HandlerName registered on the JS side
- (void)flushMessageQueue:(NSString *)messageQueueString; // Refresh message queue, core code
- (void)injectJavascriptFile; / / JS
- (BOOL)isWebViewJavascriptBridgeURL:(NSURL*)url; / / determine whether WebViewJavascriptBridgeURL
- (BOOL)isQueueMessageURL:(NSURL*)urll; // Determine if it is a queue message URL
- (BOOL)isBridgeLoadedURL:(NSURL*)urll; // Determine whether to load the URL for bridge
- (void)logUnkownMessage:(NSURL*)url; // Prints an unknown message received
- (NSString *)webViewJavascriptCheckCommand; // JS bridge check command
- (NSString *)webViewJavascriptFetchQueyCommand; // JS bridge gets the query command
- (void)disableJavscriptAlertBoxSafetyTimeout; // Disable the JS AlertBox security duration to improve the sending speed. This function is not recommended for the reasons described above

@end
Copy the code

~ from. H file we can see the whole information exposed by WebViewJavascriptBridgeBase, attribute level need to the following four attributes to deepen impression, after analysis in the process of implementation will be brought into these properties:

  • id <WebViewJavascriptBridgeBaseDelegate> delegateProxy, which enables the current Bridge-bound WebView component to execute JS code
  • NSMutableArray* startupMessageQueue;Start a message queue to store messages sent by Obj-C to JSWVJBMessageType)
  • NSMutableDictionary* responseCallbacks;Call back blocks dictionary, storeWVJBResponseCallbackThe type of block,
  • NSMutableDictionary* messageHandlers;Handlers dictionary that is registered at the obj-C endWVJBHandlerThe type of block,

Emmmmm… Interface level take a look at the notes, after the analysis of the implementation will be accompanied by some interface, the rest of the interface content unrelated to the implementation of interested students recommend their own source code ha.

We have an initial impression on WebViewJavascriptBridgeBase whole can write a page on your own, after simple embedded some JS ran over the process, in the middle the breakpoint gathers up the source code, so our interactive process of Native and JS can clear.

Under the simulated JS again by WebViewJavascriptBridge calling Native functions related implementation process analysis WebViewJavascriptBridgeBase (now consider the timing of the decision in WKWebView, for example, Namely for WKWebViewJavascriptBridge source) :

1. Listen for the fake Request and inject javascript code in WebViewJavascriptBridge_JS

As mentioned above, the implementation of WebViewJavascriptBridge is essentially realized by using the fake Request method mentioned in my previous article “summary of iOS and JS interactive development knowledge”, so let’s start by listening to fake Request.

// The WKNavigationDelegate protocol method, which listens for the Request and decides whether to allow navigation
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^) (WKNavigationActionPolicy))decisionHandler {
    / / the webView check
    if(webView ! = _webView) {return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;

    // Core code
    if ([_base isWebViewJavascriptBridgeURL:url]) { / / determine WebViewJavascriptBridgeURL
        if ([_base isBridgeLoadedURL:url]) { / / determine BridgeLoadedURL
            // Inject JS code
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) { / / determine QueueMessageURL
            // Refresh the message queue
            [self WKFlushMessageQueue];
        } else {
            // Record unknown Bridge MSG logs
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    
    Call the delegate method corresponding to _webViewDelegate
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
        [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
    } else {
        decisionHandler(WKNavigationActionPolicyAllow); }}Copy the code

Note: Said before WebViewJavascriptBridge WebView will hook binding agent method, this WKWebViewJavascriptBridge, too, After adding your own code, it determines if any _webViewDelegate responds to the proxy method, and if so, calls it.

Let’s focus on the position of the core code in the comment, which determines whether the current URL is a bridge URL:

// Related macro definitions
#define kOldProtocolScheme @"wvjbscheme"
#define kNewProtocolScheme @"https"
#define kQueueHasMessage   @"__wvjb_queue_message__"
#define kBridgeLoaded      @"__bridge_loaded__"
Copy the code

WebViewJavascriptBridge making use of the page in step 4 clearly points to copy and paste setupWebViewJavascriptBridge method into the front JS, let’s take a look at this period of JS method source code:

function setupWebViewJavascriptBridge(callback) {
	if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
	if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
	window.WVJBCallbacks = [callback];
	// Create an iframe
	var WVJBIframe = document.createElement('iframe');
	// Set iframe to no display
	WVJBIframe.style.display = 'none';
	// 将 iframe 的 src 置为 'https://__bridge_loaded__'
	WVJBIframe.src = 'https://__bridge_loaded__';
	// Add iframe to document.documentElement
	document.documentElement.appendChild(WVJBIframe);
	setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)}Copy the code

The above code creates an undisplayed iframe and sets its SRC to https://__bridge_loaded__, as defined above by the kbridgeloadedurl macro, which is used for isBridgeLoadedURL: Method to determine whether the current URL is BridgeLoadedURL.

Note: There are two ways to initiate a fake Request: -1:location.href -2:iframe. There is a problem with location.href. If JS calls the Native method several times, that is, the value of location.href changes several times, the Native end can only accept the last request, and the previous request will be ignored. Therefore, WebViewJavascriptBridge uses iframe, which will not be explained later.

Since the iframe element SRC https://__bridge_loaded__ is added, the proxy method that intercepts the URL above gets the URL https://__bridge_loaded__. Because of satisfying determine WebViewJavascriptBridgeURL HTTPS, will enter the core code area and then will be judged BridgeLoadedURL injection JS code execution method, namely [_base injectJavascriptFile]; .

- (void)injectJavascriptFile {
    // Get the WebViewJavascriptBridge_JS code
    NSString *js = WebViewJavascriptBridge_js();
    // Inject the obtained JS into the currently bound WebView component through the proxy method
    [self _evaluateJavascript:js];
    // If there is a message queue, the message is traversed and distributed, then the message queue is emptied
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self_dispatchMessage:queuedMessage]; }}}Copy the code

At this point, the first interaction is complete. The javascript code inside WebViewJavascriptBridge_JS will be read in the following chapters. Now it can be simply understood as the specific implementation code of WebViewJavascriptBridge on the JS side.

2. JS end callcallHandlerHow does the Native end respond after the method?

WebViewJavascriptBridge GitHub page indicates the operation mode of JS side:

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

We know JS end call setupWebViewJavascriptBridge method will go analysis we have just the first step to the listening false Request and inject WebViewJavascriptBridge_JS within the JS code. So when the JS side calls bridge.callHandler, how does the Native side respond? Here we need to read the javascript code in the WebViewJavascriptBridge_JS that we injected earlier:

// Call iOS handler, and call _doSend after parameter verification
function callHandler(handlerName, data, responseCallback) {
	if (arguments.length == 2 && typeof data == 'function') {
		responseCallback = data;
		data = null;
	}
	_doSend({ handlerName:handlerName, data:data }, responseCallback);
}

Message ['callbackId'] and responseCallbacks[callbackId]
// Add MSG to sendMessageQueue array and set messagingiframe.src
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;
}
	
// Scheme uses HTTPS to match with host
var CUSTOM_PROTOCOL_SCHEME = 'https';
var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
Copy the code

HandlerName and data are used as dictionary parameters to call the _doSend method. Let’s look at the _doSend method:

  • _doSendThe method internally determines whether there is a callback in the input parameter
  • The callback, if any, is generated according to the rulecallbackIdAnd save the callback block toresponseCallbacksDictionary, and then add a key/value pair to the message to save the generated onecallbackId
  • aftersendMessageQueueThe queue to joinmessage
  • willmessagingIframe.srcSet tohttps://__wvjb_queue_message__

Okay, so far, for WebViewJavascriptBridge_JS JS side of the other source code we put behind to see. Note the addition of a messagingIframe with a SRC of https://__wvjb_queue_message__, which is also an invisible iframe. In this way, the Native end will receive a request with a URL of https://__wvjb_queue_message__. After obtaining a false request in Step 1, it will make various judgments. This time the determination of [_base isQueueMessageURL: URL] is satisfied by calling the Native WKFlushMessageQueue method.

- (void)WKFlushMessageQueue {
    / / execution WebViewJavascriptBridge. _fetchQueue (); methods
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if(error ! =nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        // Refresh the message list
        [_base flushMessageQueue:result];
    }];
}

- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
Copy the code

Visible to the Native side will refresh the queue invokes the JS WebViewJavascriptBridge. _fetchQueue (); Method, we look at the JS side of this method concrete implementation:

// Get the queue. This function is called when the message queue is refreshed on iOS
function _fetchQueue() {
   // Convert sendMessageQueue to JSON format
	var messageQueueString = JSON.stringify(sendMessageQueue);
	/ / reset sendMessageQueue
	sendMessageQueue = [];
	// Return JSON
	return messageQueueString;
}
Copy the code

This method returns the current JS sendMessageQueue message queue as JSON, and the Native side calls [_base flushMessageQueue:result]; Call the flushMessageQueue: method with the JSON message queue as an argument. This method is the essence of the Native end of the framework, but it’s a bit long.

- (void)flushMessageQueue:(NSString *)messageQueueString {
    / / check 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;
    }

    // Parse messageQueueString to messages via NSJSONSerialization and iterate over it
    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        // Type verification
        if(! [message isKindOfClass:[WVJBMessageclass]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];
        
        // Try to get the responseId, if it is a callback, get the matching callback block from _responseCallbacks and execute
        NSString* responseId = message[@"responseId"];
        if (responseId) { Get responseId / /
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else { // The responseId is not returned, indicating that a normal JS callHandler is calling iOS
            WVJBResponseCallback responseCallback = NULL;
            // Try callbackId, example cb_1_1512035076293
            / var/JS code callbackId = 'cb_' + (uniqueId++) + '_' + new Date (). The getTime ();
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) { // Get the callbackId, indicating that the JS side wants a callback after calling the iOS Native code
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                    // call callbackId as responseId of MSG and set responseData to _queueMessage
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    // the queuemessage function converts MSG to JSON with responseId = callbackId
                    // The JS side calls WebViewJavascriptBridge._handleMessageFromObjC('msg_JSON'); Where 'msg_JSON' is MSG in JSON format
                    [self _queueMessage:msg];
                };
            } else { // No callbackId was obtained
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            // Try to get a previously registered iOS handler with handlerName
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]].if(! handler) {// if not registered, skip this MSG
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            // Call the responseCallback handler with message[@"data"] as the input
            handler(message[@"data"], responseCallback); }}}Copy the code

FlushMessageQueue: As the core of the whole Native end, it’s understandable that the method is a bit long. Let’s briefly explain how to implement it:

  • Into the reference check
  • Convert jSON-formatted input parameters to Native objects, i.e. message queues, where the message type is previously defined as WVJBMessage, i.e. dictionary
  • If the message contains “responseId”, it indicates that the message was called back by the JS method called by Native (since the implementation logic of JS side and Native side is equivalent, you can refer to the following analysis if you don’t understand this part).
  • If the message does not contain “responseId”, it indicates that the JS side passedcallHandlerThe function normally calls the message from Native
  • Try to get the “callbackId” in the message. If the JS message needs Native response, the callback will have this key value. For details, see the JS side above_doSendPartial source code analysis. If “callbackId” is obtained, a callback block is generated, and “callbackId” is executed as “responseId” of MSG inside the callback block_queueMessageSend the message to the JS side (the JS side handles the message logic in the same way as the Native side, so it is easy to understand if the current message is the message passed by the callback method)
  • Try the “handlerName” in the message frommessageHandlers(As mentioned above, it is the dictionary storing the handler registered at the Native end.) Fetch the corresponding handler block. If so, execute the code block; otherwise, print the error log

Note: Although this message processing method is long, it has clear logic, and effectively solves the problem of parameter transfer (including callback) in the process of mutual invocation between JS and Native. In addition, the message processing logic of JS terminal is consistent with that of Native terminal, realizing logical symmetry, which is worth learning.

Webviewjavascriptbridge_js-native calls JS to implement interpretation

Emmmmm… This chapter mainly talks about the JS side injection code, that is, the JS source code in WebViewJavascriptBridge_JS. Because I have not done the previous paragraph, the ability is insufficient, the level is limited, there may be fallacy hope that the reader found the words timely correction, grateful. Warning, because the JS side and the above analysis of the logic symmetry of the Native side and the above analysis of part of the JS side of the function, so the following JS source code is not split, in order to avoid being a large section of JS code confused face uninterested students can directly see the code behind the summary.

; (function() {
    / / window. WebViewJavascriptBridge check, avoid duplication
	if (window.WebViewJavascriptBridge) {
		return;
	}

    // Lazy load window.onerror, used to print error logs
	if (!window.onerror) {
		window.onerror = function(msg, url, line) {
			console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":"+ line); }}/ / window. WebViewJavascriptBridge statement
	window.WebViewJavascriptBridge = {
		registerHandler: registerHandler,
		callHandler: callHandler,
		disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	};

    // Variable declaration
	var messagingIframe; / / message iframe
	var sendMessageQueue = []; // Send message queue
	var messageHandlers = {}; Handlers dictionary; // Handlers dictionary;
	
	// Scheme uses HTTPS to match with host
	var CUSTOM_PROTOCOL_SCHEME = 'https';
	var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
	
	var responseCallbacks = {}; // the JS side stores the callback dictionary
	var uniqueId = 1; // A unique identifier used to generate a callbackId during a callback
	var dispatchMessagesWithTimeoutSafety = true; // Security duration is enabled by default

    // Speed bridge messaging by disabling AlertBoxSafetyTimeout
    function disableJavscriptAlertBoxSafetyTimeout() {
		dispatchMessagesWithTimeoutSafety = false;
	}

    // Registering handlers is like adding a block to the messageHandlers dictionary
	function registerHandler(handlerName, handler) {
		messageHandlers[handlerName] = handler;
	}
	
	// Call iOS handler, and call _doSend after parameter verification
	function callHandler(handlerName, data, responseCallback) {
	    // If there are only two arguments and the second argument is of type function, no arguments are passed, i.e. data is null
		if (arguments.length == 2 && typeof data == 'function') {
			responseCallback = data;
			data = null;
		}
		// Call _doSend with handlerName and data as MSG object arguments
		_doSend({ handlerName:handlerName, data:data }, responseCallback);
	}
	
	// _doSend Sends messages to the Native end
	function _doSend(message, responseCallback) {
	    Message ['callbackId'] and responseCallbacks[callbackId]
		if (responseCallback) {
			var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
			responseCallbacks[callbackId] = responseCallback;
			message['callbackId'] = callbackId;
		}
		// Add MSG to sendMessageQueue array and set messagingiframe.src
		sendMessageQueue.push(message);
		messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
	}

    // Get the queue. This function is called when the message queue is refreshed on iOS
	function _fetchQueue() {
	    // Internally convert sendMessageQueue to JSON format and return
		var messageQueueString = JSON.stringify(sendMessageQueue);
		sendMessageQueue = [];
		return messageQueueString;
	}

	// This function is called by the _dispatchMessage function on iOS
	function _handleMessageFromObjC(messageJSON) {
	    // Dispatches messages received from Native
        _dispatchMessageFromObjC(messageJSON);
	}
    
    // Core code, scheduling the message from the Native end, logic and Native end consistent
	function _dispatchMessageFromObjC(messageJSON) {
		/ / judgment have disable AlertBoxSafetyTimeout, will eventually call _doDispatchMessageFromObjC function
		if (dispatchMessagesWithTimeoutSafety) {
			setTimeout(_doDispatchMessageFromObjC);
		} else {
			 _doDispatchMessageFromObjC();
		}
		
		// parse msgJSON to get MSG
		function _doDispatchMessageFromObjC() {
			var message = JSON.parse(messageJSON);
			var messageHandler;
			var responseCallback;

			// If (responseId = responseId); // If (responseId = responseId)
			if (message.responseId) {
				responseCallback = responseCallbacks[message.responseId];
				if(! responseCallback) {return;
				}
				responseCallback(message.responseData);
				delete responseCallbacks[message.responseId];
			} else { // If there is no responseId, the normal iOS call handler is calling js
				// If MSG contains callbackId, iOS needs to callback to initialize the corresponding responseCallback
				if (message.callbackId) {
					var callbackResponseId = message.callbackId;
					responseCallback = function(responseData) {
						_doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
					};
				}
				
				// Get the corresponding handler from messageHandlers to execute
				var handler = messageHandlers[message.handlerName];
				if(! handler) {// If no handler is found, an error log is generated
					console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
				} else{ handler(message.data, responseCallback); }}}}// Declaration of messagingIframe, type iframe, style not visible, SRC set
	messagingIframe = document.createElement('iframe');
	messagingIframe.style.display = 'none';
	messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
	// Add messagingIframe to document.documentElement
	document.documentElement.appendChild(messagingIframe);

    / / registered disableJavscriptAlertBoxSafetyTimeout handler, Native can disable AlertBox time to speed up the safety of the bridge
	registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
	
	setTimeout(_callWVJBCallbacks, 0);
	function _callWVJBCallbacks() {
		var callbacks = window.WVJBCallbacks;
		delete window.WVJBCallbacks;
		for (var i=0; i<callbacks.length; i++) { callbacks[i](WebViewJavascriptBridge); }}}Copy the code

The logic of the JS side is consistent with that of the Native side. The above code has been added with detailed Chinese annotations. In the above “WebViewJavascriptBridgeBase – JS calls Native implementation principle analyzes” chapter in the process of analysis in order to go through the whole call logic has been analyses some JS client-side code, Here we simply comb JS side core code _doDispatchMessageFromObjC function logic:

  • Parse messageJSON using JSON
  • Try to get the responseId in the parsed message. If it gets the responseId, it indicates that the Native end responds to the message sent by the JS end through the callback. Then use the responseId to get the corresponding callback response block in the responseCallbacks. Once found, execute the block and delete
  • If the responseId is not returned, the message is NativecallHandler:data:responseCallback:A message sent by a handler registered with JS is normally called (normal here for callbacks).
  • If the current message has a callbackId, it indicates that the Native terminal needs the JS terminal to respond to the callback feedback after the message and generate a responseCallback as a callback block (JS terminal is function), which is used internally_doSendThe responseId () method passes a message with the responseId to the Native endpoint, indicating that this message is the previous callback message
  • Finally, according to the handlerName in the parsed message, the handlers corresponding to the name are found from messageHandlers, that is, the handlers registered at the JS side. If not, an error log with relevant information is printed

FlushMessageQueue: It’s easy to see that the processing implementations at both ends are logically symmetric.

WebViewJavascriptBridge’s “bridge aesthetics”

Before summarizing the “bridge aesthetics” of WebViewJavascriptBridge, please review the workflow of WebViewJavascriptBridge:

  • SRC is added to the JS sidehttps://__bridge_loaded__iframe
  • The Native end detects the Request and checks if yes__bridge_loaded__Inject the WebView javascriptBridge_js code through the current WebView component
  • After the injected code succeeds, a messagingIframe is added, whose SRC ishttps://__wvjb_queue_message__
  • After that, both the Native end and the JS end can passregisterHandlerMethod to register a handler with a specified HandlerName can also passcallHandlerMethod calls the processing on the other end with the agreed HandlerName (implementation of logical symmetry for processing messages on both ends)

So it’s easy to list the “aesthetics” of WebViewJavascriptBridge:

  • The recessive adaptation
  • Interface peer
  • Logical symmetry

We combine this article to expand the above “aesthetic” concrete implementation.

The recessive adaptation

WebViewJavascriptBridge is mainly used as a bridge for Mac OS X, Native iOS terminal and JS terminal to communicate with each other and call each other. For the three WebView functional components contained in Mac OS X and iOS platforms, WebView javascriptBridge makes implicit adaptation, that is, it can bind WebView components of different platforms to achieve JS communication function with the same function with only one set of code. This is very convenient.

Interface peer

WebViewJavascriptBridge designs equivalent interfaces for the JS end and the Native end. Whether it is the JS end or the Native end, the response processing of the registered local end uses registerHandler interface. Calling the other end (sending messages to the other end) is done using the callHandler interface.

This is very reasonable, because both the JS end and the Native end are in an equal position in terms of communication itself. This is just like a bridge connecting two lands. The two lands use the bridge to transport goods and receive resources from each other. The two lands are logically equivalent in the transport and use of the bridge.

Logical symmetry

WebViewJavascriptBridge has the same logical processing implementation for the message sent from the JS side and the Native side. Considering the identity of the sender and the receiver, the same logic can be regarded as logical symmetry.

This implementation is still very reasonable, the two continents connected by the bridge should be logically symmetric in loading the upper bridge and unloading the lower bridge.

Well, I have to sacrifice a word to describe the WebViewJavascriptBridge, the word is elegant (laugh). When you combine WebViewJavascriptBridge source code after reading this article, it is not difficult to find that its entire architecture and design ideas coincide with many design ideas in real bridge design. For example, the bridge is generally divided into left and right bridge width, and the left and right bridge width is generally only one line center line, that is, one forward direction. Used for resource transmission in a single direction on the bridge, the left and right bridge width is functionally equivalent.

The article summarizes

  • This article systematically analyzes the source code of WebViewJavascriptBridge. I hope you can have an overall understanding of the architecture of WebViewJavascriptBridge after reading this article.
  • This article makes an in-depth analysis of the message processing implementation of WebViewJavascriptBridge in JS side and Native side, hoping to provide some meager help for readers to understand this part of the source code.
  • It summarizes the advantages of WebViewJavascriptBridge as a JSBridge framework, that is, the “bridge aesthetics” referred to in the paper. It is expected to provide ideas for everyone to encapsulate a JSBridge in the future and throw a brick to attract jade.

Emmmmm… However, it is important to note that WebViewJavascriptBridge is only used as a JSBridge layer to provide basic support for passing messages between JS and Native. If you want to encapsulate the WebView component in your project, you also need to implement HTTP cookie injection, user-defined User-Agent, whitelist or permission verification and other functions. Further, you also need to optimize the initialization speed of WebView component, page rendering speed and page cache strategy. I’ll probably probably write a post about some of the pitfalls and experiences I’ve had in encapsulating WebView components, since I’m limited… So maybe not (laughs).

The article is carefully written (it is my personal original article, please indicate lision. Me/for reprinting). If any mistake is found, it will be updated in my personal blog first. If you have any questions, please feel free to contact me on my Weibo @lision


Supplement ~ I set up a technical exchange wechat group, want to know more friends in it! If you have any questions about the article or encounter some small problems in your work, you can find me or other friends in the group to communicate and discuss. Looking forward to your joining us

Emmmmm.. Because the number of wechat group is over 100, it is not possible to scan the code into the group, so please scan the qr code above to pay attention to the public number into the group.