preface

The previous article introduced the relevant technologies of mobile terminal development, and this one mainly started from the JS Bridge communication developed by Hybrid.

As the name implies, JS Bridge means Bridge, which is a Bridge connecting JS and Native, and also the core of Hybrid App. It is generally divided into TWO forms: JS invocation Native and Native active JS invocation.

URL Scheme

URL Scheme is a special URL. It is generally used to wake up the App on the Web or even jump to a page of the App. For example, when you make payment on a mobile website, you can directly pull up the Alipay payment page.

Here is a small example. You can type weixin:// directly into the browser, and the system will prompt you whether to open wechat. Type MQQ :// and it will remind you of QQ on your phone.

Here’s a round-up of common App URL Schemes: URL Schemes

After opening this page in the phone, click here and you will be prompted to open wechat or not.

Deeplink is also based on a URL Scheme. The structure of a URI is as follows:

URI = scheme:[//authority]path[?query][#fragment]
// scheme = http
// authority = www.baidu.com
// path = /link
// query = url=xxxxx
authority = [userinfo@]host[:port]
Copy the code

In addition to HTTP or HTTPS, you can customize protocols. To borrow a picture from Wikipedia:

Usually, after the App is installed, a Scheme will be registered in the mobile system, such as weixin://, so when we access the Scheme address in the mobile browser, the system will invoke our App.

In Android, you need to register Scheme in the Androidmanifest.xml file:

<activity
    android:name=".login.dispatch.DispatchActivity"
    android:launchMode="singleTask"
    android:theme="@style/AppDispatchTheme">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="taobao" />
        <data android:host="xxx" />
        <data android:path="/goods" />
    </intent-filter>
</activity>
Copy the code

In iOS, you need to register in Xcode, some are already used by the system should not be used, such as Maps, YouTube, Music. See the Apple Developer’s official documentation for Defining a Custom URL Scheme for Your App

JS call Native

In iOS, we need to distinguish between UIWebView and WKWebView:

WKWebView was introduced after iOS8 to replace the clunky UIWebView. It takes up less memory, about a third of UIWebView, supports better HTML5 features, and is much more powerful. However, there are some disadvantages, such as no cache support, need to inject their own cookies, send A POST request with no parameters, interception of THE POST request can not parse parameters, and so on.

There are roughly three methods for JS to invoke Native communication:

  1. Intercept Scheme
  2. Pop-up blocking
  3. Injecting JS context

These three methods have advantages and disadvantages in general, and will be introduced one by one below.

Intercept Scheme

If you think about it, what do you do when you pass data between JS and Java? Invoking the Ajax request interface is one of the most common requirements for front-end development. Regardless of whether it’s Java or Python, we can get data through the HTTP/HTTPS interface. In fact, the process is more similar to JSONP.

Given that clients can intercept requests, is it possible to build on this?

What if we request a nonexistent address with parameters that tell the client what function we need to call?

For example, if I want to call scan:

axios.get('http://xxxx? func=scan&callback_id=yyyy')Copy the code

The client can intercept the request to parse the func above the parameter to determine which function needs to be invoked. After the client invokes the sweep function, it retrieves the Callbacks object on the WebView and calls it back based on the callBack_id.

So based on the example above, we can treat the domain name and path as communication identifiers, the func in the parameter as instructions, callback_id as callback functions, and other parameters as data passing. HTTP requests that do not meet the criteria should not be intercepted.

Of course, the current mainstream approach is the custom Scheme protocol we saw earlier, with this as the communication identifier, domain name and path as instructions.

The nice thing about this approach is that iOS6 used to only support this approach, so it’s compatible.

JS side

There are many ways to initiate a request, but the most widely used is the iframe jump:

  1. Use the A label to jump
<a href="taobao://"> </a>Copy the code
  1. redirect
location.href = "taobao://"
Copy the code
  1. The iframe jump
const iframe = document.createElement("iframe");
iframe.src = "taobao://"
iframe.style.display = "none"
document.body.appendChild(iframe)
Copy the code

The Android side

On Android, shouldOverrideUrlLoading can be used to intercept URL requests.

@Override public boolean shouldOverrideUrlLoading(WebView view, String url) {if (url.startswith ("taobao")) {// Call Native method return true; }}Copy the code

The iOS side

On iOS, you need to distinguish between UIWebView and WKWebView. In a UIWebView:

- (BOOL)shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(BPWebViewNavigationType)navigationType { if (XXX) {// Call the Native method based on the instructions and parameters of the call path; } return [super shouldStartLoadWithRequest:request navigationType:navigationType]; }Copy the code

In WKWebView:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(nonnull WKNavigationAction *)navigationAction DecisionHandler :(nonnull void (^)(WKNavigationActionPolicy))decisionHandler {if(XXX) { According to these to call Native methods BLOCK_EXEC (decisionHandler WKNavigationActionPolicyCancel); } else { BLOCK_EXEC(decisionHandler, WKNavigationActionPolicyAllow); } [self.webView.URLLoader webView:webView decidedPolicy:policy forNavigationAction:navigationAction]; }Copy the code

Currently, it is not recommended to use only the form of intercepting URL Scheme to resolve parameters. There are several main problems.

  1. Continuous calllocation.hrefMessage loss occurs because the WebView limits continuous jumps and filters out subsequent requests.
  2. Urls are limited in length, and if they are too long, information will be lost

Therefore, libraries such as WebViewJavaScriptBridge are used together with the form of injection API, which is also the way we currently use, and will be introduced later.

Pop-up blocking

Android implementation

This method is to use the popover will trigger the corresponding WebView event to intercept. The onJsAlert, onJsConfirm and onJsPrompt methods in setWebChromeClient intercept and parse messages from them.

@override public Boolean onJsPrompt(WebView view, String URL, String message, String defaultValue, JsPromptResult result) {if (XXX) { } return super.onJsPrompt(view, URL, message, defaultValue, result); Override public Boolean onJsConfirm(WebView view, String url, String message, JsResult result) { return super.onJsConfirm(view, url, message, result); } // Alert @override public Boolean onJsAlert(WebView view, String url, String message, JsResult result) { return super.onJsAlert(view, url, message, result); }Copy the code

The iOS implementation

Let’s take WKWebView as an example:

+ (void)webViewRunJavaScriptTextInputPanelWithPrompt:(NSString *)prompt
    defaultText:(NSString *)defaultText
    completionHandler:(void (^)(NSString * _Nullable))completionHandler
{
    /** Triggered by JS:
    var person = prompt("Please enter your name", "Harry Potter");
    if (person == null || person == "") {
       txt = "User cancelled the prompt.";
    } else {
       txt = "Hello " + person + "! How are you today?";
    }
    */
    if (xxx) {
        BLOCK_EXEC(completionHandler, text);
    } else {
        BLOCK_EXEC(completionHandler, nil);
    }
 }

Copy the code

The disadvantage of this approach is that UIWebView is not supported on iOS, but WKWebView has better scriptMessageHandler, which is embarrassing.

Injection context

As mentioned before, the framework JavaScriptCore is built into iOS, which can implement functions such as JS execution and Native object injection.

This approach does not rely on interception, mainly through the WebView to inject objects and methods into the JS context, can let JS directly call the native.

PS: A Block in iOS is OC’s implementation of a closure, which is essentially an object that defines a function in JS.

iOS UIWebView

IOS side code:

/ / get JS context JSContext * context = [webview valueForKeyPath: @ "documentView. Webview. MainFrame. JavaScriptContext"]. [@"callHandler"] = ^(JSValue * data) {// Call Native methods and arguments // call JS Callback}Copy the code

JS code:

window.callHandler({
    type: "scan",
    data: "",
    callback: function(data) {
    }
});
Copy the code

The nice thing about this approach is that the JS calls are synchronous and the return value can be retrieved immediately.

We also don’t need to send json.stringify every time we pass a value, as we do in interceptions. We can send JSON directly, and we can also send a function directly.

iOS WKWebView

WKWebView inside injection by addScriptMessageHandler object to JS context, can be invoked when the WebView destroyed removeScriptMessageHandler to destroy the object. After the front end invokes the injected native method, it can receive the parameters passed by the front end via the didReceiveScriptMessage.

WKWebView *wkWebView = [[WKWebView alloc] init]; WKWebViewConfiguration *configuration = wkWebView.configuration; WKUserContentController *userCC = configuration.userContentController; / / injection objects [userCC addScriptMessageHandler: self name: @ "nativeObj"]. / / remove object [userCC removeScriptMessageHandler: self name: @ "nativeObj"]. - (void)userContentController:(WKUserContentController *)userContentController DidReceiveScriptMessage :(WKScriptMessage *)message {NSDictionary *msgBody = message.body; // If nativeObj = nativeObj [message.name isEqualToString:@"nativeObj"]) { // return; }}Copy the code

An object injected with addScriptMessageHandler actually has only one postMessage method and no more custom methods can be called. The front end is called as follows:

window.webkit.messageHandlers.nativeObj.postMessage(data);
Copy the code

Note that this approach requires iOS8 and above, and the return is not synchronous. Like UIWebView, it supports passing JSON objects directly, without stringify.

Android addJavascriptInterface

Before Android 4.2, addJavascriptInterface was used to inject JS. AddScriptMessageHandler is similar to addScriptMessageHandler, but without its limitations.

public void addJavascriptInterface() { mWebView.addJavascriptInterface(new DatePickerJSBridge(), "DatePickerBridge"); } private class PickerJSBridge { public void _pick(...) {}}Copy the code

Call from inside JS:

window.DatePickerBridge._pick(...)
Copy the code

The @javascriptinterface annotation is available in Android4.2 and must be used to expose javascript methods. So the previous _pick method needs this annotation.

private class PickerJSBridge { @JavascriptInterface public void _pick(...) {}}Copy the code

Call Native JS

Native JS calls are usually direct JS code strings, similar to how we call EVAL in JS to execute a string of code. There are loadUrl, evaluateJavascript, etc., which are introduced here. Either way, the client can only get the properties and methods mounted on the Window object.

Android

In Android, versions need to be different. Prior to Android 4.4, loadUrl is supported in the same way that we write JS scripts in the href of a tag, which are in the form of javascript: XXX. You can’t get the return value directly in this way.

webView.loadUrl("javascript:foo()")
Copy the code

EvaluateJavascript is commonly used in Android versions 4.4 and up. Here we need to determine the version.

if (Build.VERSION.SDK_INT > 19) //see what wrapper we have
{
    webView.evaluateJavascript("javascript:foo()", null);
} else {
    webView.loadUrl("javascript:foo()");
}
Copy the code

UIWebView

Used in iOS UIWebView stringByEvaluatingJavaScriptFromString to invoke the JS code. This approach is synchronous and blocks the thread.

results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];
Copy the code

WKWebView

WKWebView can use the evaluateJavaScript method to call JS code.

[self.webView evaluateJavaScript:@"document.body.offsetHeight;" completionHandler:^(id _Nullable response, NSError * _Nullable error) {response}];Copy the code

JS Bridge design

After all the methods of JS and Native intermodulation have been mentioned above, let’s introduce the design of JS Bridge here. Our JS Bridge communication is based on the library WebViewJavascriptBridge to achieve. It is mainly combined with Scheme protocol + context injection to do. Considering the different communication modes of Android and iOS, encapsulation is implemented to ensure that the API provided to the outside is consistent. The specific function calls are packaged into the NPM package. Here are some basic apis:

  1. CallHandler (name, params, callback) : This is a method that calls Native functions, passing module names, parameters, and callback functions to Native.
  2. HasHandler (name) : This is a call to check whether the client supports a function.
  3. RegisterHandler (name) : This is to pre-register a function and wait for a Native callback, for examplepageDidBackThis kind of scenario.

So how are these apis implemented? The Android and iOS packages are inconsistent here and should be described separately.

Android Bridge

Previously we said that Android can use the @javascriptInterface annotation to expose objects and methods to JS. So several methods here are called by annotation exposed to JS, doing some compatibility processing at JS level.

hasHandler

The first and simplest is the hasHandler, which maintains a table of supported Bridge modules in the client. Just use the switch… Just make a judgment.

@JavascriptInterface
public boolean hasHandler(String cmd) {
        switch (cmd) {
            case xxx:
            case yyy:
            case zzz:
                return true;
        }
        return false;
    }
Copy the code

callHandler

Then we look at callHandler, which is a method that provides JS to call Native functions. Before calling this method, we generally need to determine whether Native supports this functionality.

function callHandler(name, params, callback) {
    if (!window.WebViewJavascriptBridge.hasHandler(name)) {
    }
}
Copy the code

If Native does not support the Bridge, we need to make the callback compatible. This compatibility process consists of two aspects: the functional aspect and the default callback argument for callback.

For example, if the client does not support the Bridge, or if we open the page in the browser, we should exit to the Alert popup using the Web. For callback, we can pass 0 by default to indicate that this function is not currently supported.

Assuming that the bridge of this alert receives two parameters, title and Content, it should be displayed using the browser’s own alert.

function fallback(params, callback) {
    let content = `${params.title}\n{params.content}`
    window.alert(content);
    callback && callback(0)
}
Copy the code

We want the fallback function to be more generic, and each calling method should have its own fallback function, so the previous callHandler should look like this:

function callHandler(name, params, fallback) { return function(... rest, callback) { const paramsList = {}; for (let i = 0; i < params.length; i++) { paramsList[params] = rest[i]; } if (! callback) { callback = function(result) {}; } if (fallback && ! window.WebViewJavascriptBridge.hasHandler(name))) { fallback(paramsList, callback); } else { window.WebViewJavascriptBridge.callHandler(name, params, callback); }}}Copy the code

We can encapsulate some function methods based on this function, such as the previous alert:

function fallback(params, callback) {
    let content = `${params.title}\n{params.content}`
    window.alert(content);
    callback && callback(0)}function alert(title, content, cb: any) {
  return callHandler(
    'alert'['title'.'content'],
    fallback
  )(title, content, cb);
}
alert(`this is title`.`hahaha`.function() {
    console.log('success')})Copy the code

The effect is similar to the following, which is a random image from Google:

How does the client implement the callback function? As mentioned earlier, when a client wants to call a JS method, it can only call one mounted on the Window object.

So, this is a neat way to do it, and the callback function is still JS. Before calling Native, we can map the callback function to a uniqueId and store it locally in JS. All we need to do is pass the callbackId to Native.

function callHandler(name, data, callback) {
    const id = `cb_${uniqueId++}_${new Date().getTime()}`;
    callbacks[id] = callback;
    window.bridge.send(name, JSON.stringify(data), id)
}
Copy the code

On the client side, when the send method receives the parameters, it performs the corresponding function and proactively calls one of the front-end receiving functions using webView.loadURL.

@javascriptInterface public void send(final String CMD, String data, final String callbackId) { Format = String. Format ("javascript: window.bridge.onReceive(\'%1$s\', \'%2$s\');" , callbackId, result.toDataString()); webView.loadUrl(js); }Copy the code

So JS needs to define the onReceive method beforehand, which receives a callbackId and a result.

window.bridge.onReceive = function(callbackId, result) { let params = {}; try { params = JSON.parse(result) } catch (err) { // } if (callbackId) { const callback = callbacks[callbackId]; callback(params) delete callbacks[callbackId]; }}Copy the code

The general process is as follows:

registerHandler

The registration process is relatively simple, and we store the callback function in a messageHandler object, but this time the key is not a random ID, but a name.

function registerHandler(handlerName, callback) {
    if(! messageHandlers[handlerName]) { messageHandlers[handlerName] = [handler]; }else {
      // Multiple handlers can be registeredmessageHandlers[handlerName].push(handler); }}MessageHandlers are directly checked to see if they are present
function hasRegisteredHandler(handlerName) {
    let has = false;
    try{ has = !! messageHandlers[handlerName]; }catch (exception) {}
      return has;
    }
Copy the code

Unlike callHandler, which notifies the client with window.bridge.send, it simply waits for the client to call window.bridge.onReceive at the appropriate time. So the onReceive method needs to be tweaked here. Since there will no longer be a callbackId, the client can pass a null value and put handlerName inside result.

window.bridge.onReceive = function(callbackId, result) {
    let params = {};
    try {
        params = JSON.parse(result)
    } catch (err) {
        //
    }
    if (callbackId) {
        const callback = callbacks[callbackId];
        callback(params)
        delete callbacks[callbackId];
    } else if (params.handlerName)(
        // There may be multiple registrations
        const handlers =  messageHandlers[params.handlerName];
        for (let i = 0; i < handlers.length; i++) {
            try {
                delete params.handlerName;
                handlers[i](params);
            } catch (exception) {
            }
        }
    )
}

Copy the code

In this case, the flow is as follows, and it can be found that JS calls Native are not needed at all:

iOS Bridge

After talking about Android, let’s talk about iOS. Originally, iOS and Android can be designed the same, but there are many differences due to various reasons.

The most significant difference between iOS and Android lies in the implementation of the window.bridge.send method. In Android, the Native method is called directly, while in iOS, the method is called in the form of URL Scheme.

The protocol is still the protocol in WebViewJavaScriptBridge. The URL Scheme itself does not pass data, but only tells Native that there is a new call.

Native then calls the JS methods to get all the methods in the queue that need to be executed.

Therefore, we need to create an iframe in advance and insert it into the DOM for future use.

const CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme';
const QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__';
function _createQueueReadyIframe(doc) {
    messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
    doc.documentElement.appendChild(messagingIframe);
  }
Copy the code

callHandler

You just need to reuse this iframe every time you call it. Here is the code that handles callback and notifies Native:

function callHandler(handlerName, data, responseCallback) {
    _doSend({ handlerName: handlerName, data: data }, responseCallback);
  }
function _doSend(message, callback) {
    if (responseCallback) {
      const callbackId = `cb_${uniqueId++}_The ${new Date().getTime()}`;
      callbacks[callbackId] = callback;
      message['callbackId'] = callbackId;
    }
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + ': / /' + QUEUE_HAS_MESSAGE;
  }
Copy the code

After notifying Native, how does it get our handlerName and data? We can implement a fetchQueue method.

  function _fetchQueue() {
    const messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;
  }
Copy the code

And then to mount it to the window. The WebViewJavascriptBridge object above.

  window.WebViewJavascriptBridge = {
    _fetchQueue: _fetchQueue
  };
Copy the code

This way iOS can easily get the messageQueue using evaluateJavaScript.

- (void)flushMessageQueue { [_webView evaluateJavaScript:@"WebViewJavascriptBridge._fetchQueue();" completionHandler:^(id _Nullable result, NSError * _Nullable error) { [self _flushMessageQueue:result]; }]; } - (void)_flushMessageQueue:(id)messageQueueObj {Copy the code

How does iOS callback to JS’s callback function? This is actually the same principle as Android onReceive. Here, too, can realize a _handleMessageFromObjC method, mounted to the window. The WebViewJavascriptBridge object above, waiting for the iOS callback.

function _dispatchMessageFromObjC(messageJSON) {
    const message = JSON.parse(messageJSON);
    if (message.responseId) {
        var responseCallback = callbacks[message.responseId];
        if(! responseCallback) {return;
        }
        responseCallback(message.responseData);
        deletecallbacks[message.responseId]; }}Copy the code

The process is as follows:

registerHandler

RegisterHandler works exactly the same as Android. It registers an event in advance and waits for iOS to call it.

// function registerHandler(handlerName, handler) { if (typeof messageHandlers[handlerName] === 'undefined') { messageHandlers[handlerName] = [handler]; } else { messageHandlers[handlerName].push(handler); Function _dispatchMessageFromObjC(messageJSON) {const message = json.parse (messageJSON); if (message.responseId) { var responseCallback = callbacks[message.responseId]; if (! responseCallback) { return; } responseCallback(message.responseData); delete callbacks[message.responseId]; } else if (message.handlerName){ handlers = messageHandlers[message.handlerName]; for (let i = 0; i < handlers.length; i++) { try { handlers[i](message.data, responseCallback); } catch (exception) { } } } }Copy the code

conclusion

These are the general principles of interaction between JS and Native in Hybrid, and many details are ignored, such as initialization of WebViewJavascriptBridge objects, etc. Those who are interested can also refer to this library: JsBridge