preface

H5 VS Native has long been a contentious topic in the front-end technology community. Technology stacks like React and VUE lead pure H5 development, while Rn and Week advocate native experiences. But in the real world, a neutral option is often chosen: mixed development. Popular name: Hybrid.

I am currently engaged in the research and development of news products, which are familiar to everyone, such as today’s Daily headlines, Baidu News, netease News, etc. In the initial stage of product design, considering the degree of difficulty to achieve some problems (such as news details page, text and text mixed, NA is not as easy to achieve as H5), some parts of the Hybrid way to develop, this paper will share some ideas in the development process, for your reference.

JSBridge solves the problem

Mixed development, the most important issue is: H5 and Native two-way communication. However, in reality, the interaction method between JS and NA is very limited, which will be explained in detail below. If the development is just a simple method call, neither to ensure the success of the call, nor to ensure that the code is concise enough. Hence JSBridge. JSBridge, is a JS implementation of Bridge, is a train of thought, can have different understanding, different code implementation. The main idea is to build a Bridge between H5 and NA, leaving a more friendly and reasonable interface at both ends.

A general method of two-way communication between H5 and NA

The following table lists the H5 communication modes and compatibility. It refers to loading H5 page with the help of Native WebView, and realizing message communication between H5 and NA through API, URL interception, global call and other forms. From the point of view of big factory, in actual combat, will choose a more compatible way.

H5 calls the NA method to comb

platform methods note
Android shouldOverrideUrlLoading Scheme interception method
Android addJavascriptInterface API
Android OnJsAlert (), onJsConfirm(), onJsPrompt ()
IOS Intercept the URL
IOS(UIwebview) JavaScriptCore API method, IOS7+ support
IOS(WKwebview) window.webkit.messageHandlers APi methods, IOS8+ support

NA Calls the H5 method to comb

platform methods note
Android loadurl()
Android evaluateJavascript() The Android 4.4 +
IOS(UIwebview) stringByEvaluatingJavaScriptFromString
IOS(UIwebview) JavaScriptCore IOS7.0 +
IOS(Wkwebview) evaluateJavaScript:javaScriptString IOS8.0 +

By combing through the above two call methods, it is not difficult to analyze that URL interception & implementation of JS is a more general and well-compatible solution between Android and IOS. The foundation of our mixed development is based on this approach.

Conventional mixed development thinking

In terms of communication between H5 and NA, the simplest and direct idea is as follows: NA intercepts the URL of H5 to obtain the message (usually by modifying the SRC of iframe). After service processing, NA executes JS (global method (3) registered in advance on H5) to call back H5 (as shown in the figure below).

H5 code implementation is as follows:

<html>
...
<body>
    <div class="content"> < div style = "box-sizing: border-box; color: RGB (50, 50, 50); line-height: 22px; font-size: 13px! Important; white-space: inherit! Important;functionVar sendSchema = (){function(action,param){
        let tempnode = document.createElement('iframe');
        tempnode.src = "bdnews://"+action+param; } / / document. (3) the H5 logic start running function addEventListener ("DOMContentLoaded".function(){
        sendschema('load_finish');
    },false);
</script>
  
...
</html>
Copy the code

Android works like this:

webView.setWebViewClient(new WebViewClient() {public Boolean shouldOverrideUrlLoading(WebView View, String URL) {// Scenario 1: intercept request, receive schemaif (url.equals("load_url") {// handle logic dosomething // return view.loadURL ("javascript:setAllContent(" + json + ");"} // Scenario 2: The end calls H5 by itself, and no request is initiatedclickbutton(){
            view.loadUrl("javascript:setAllContent(" + json + ");")}}});Copy the code

The IOS logic is as follows:

/ / initialize the webview UIWebView * view = [[UIWebView alloc] initWithFrame: self. The frame]; [view loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.xx.com"]]]; [self.view addSubview:view]; &nbsp; &nbsp; / * the webView method shouldStartLoadWithRequest / / ready to load content of the agreement when the method called, WebViewDidStartLoad // Method called when loading starts webViewDidFinishLoad // Method called when loading ends didFailLoadWithError // Method called when loading fails */ &nbsp; - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {if ([urlString hasPrefix:@"scheme://hybrid? info="]) {
        if([name isEqualToString:@"load_finish"]){
            // [self.webView setContent]; [self.webView stringByEvaluatingJavaScriptFromString:strFormat]; }}} -clickbutton(){
    [self.webView setContent];
}
Copy the code

But there are some pain points:

1) The callback function is not clear. It can be said that there is no mechanism of callback function at present, which leads to some analysis and judgment relying on callback function can not be used normally, such as: function caller, whether the call is successful or not, the exception handling of the call failure and other cases;

2) The correspondence is not clear. There are calls that look like callbacks, but don’t put them together, resulting in messy code that is hard to maintain. As shown in the above demo: sendSchema (‘load_finish’) and setAllContent originally meant to tell NA that the page is ready, and NA will plug data to the page after receiving it. Originally closely related to a pair of functions, separated to see what connection;

3) Global functions are jumbled. Ideally, if calls and callbacks were paired, registering and maintaining global functions in DEMO would be much less of a chore. Improves page readability and maintenance costs. For example, load_finish and setAllContent, only load_finish can be reserved.

4) Miscellaneous code inside the end. With H5 conventions registered in the end for calling methods, it is obvious that you also need to maintain a set of code indicating when to call them.

The above problems encountered in the development may not be detected at the beginning when there are not many functions, but with the increase of functions, the maintenance cost is very high in the later stage.

JSB scheme design

Add a middle layer between H5 and NA. This layer encapsulates how H5 and NA communicate. H5 and NA do not care about each other’s appearance and use the methods exposed in the middle layer to make function calls.

JSB interaction model

H5 interacts with NA. From the perspective of H5, it can be roughly divided into two categories: go and no return & go and no return.

The first kind of interaction model

Request logic: there is to go back, there is to go back. Here are two implementation schemes (the preliminary idea draft is as follows) :

① Function name association

letBDAPPnode = { callbacks: {}, // Invoke (Action, Params, successfnName, successFn) {this.callbacks[successFnName] = {success: successfn }; sendschema(action, params); } / / NA invoke callbackSuccess (callbackname, params) {try {BDAPPnode. CallbackFromNative (callbackname, params.true);
       } catch (e) {
           console.log('Error in error callback: ' + callbackname + '=' + e);
       }
   },
   callbackFromNative(callbackname, params, isSuccess) {
       let callback = this.callbacks[callbackname];
       if (callback) {
           if(isSuccess) { callback.success && callback.success(params); }}; }};Copy the code

(2) ID associated

let BDAPPnode = {
   callbackId: Math.floor(Math.random() * 2000000000),
   callbacks: {},
   invoke(action, params, onSuccess, onFail) {
       this.callbackId++;
       this.callbacks[self.callbackId] = {
           success: onSuccess,
           fail: onFail
       };
       sendschema(action, params, this.callbackId);
   },
   callbackSuccess(callbackId, params) {
       try {
           BDAPPnode.callbackFromNative(callbackId, params, true);
       } catch (e) {
           console.log('Error in error callback: ' + callbackId + '=' + e);
       }
   },
   callbackError(callbackId, params) {
       try {
           BDAPPnode.callbackFromNative(callbackId, params, false);
       } catch (e) {
           console.log('Error in error callback: ' + callbackId + '=' + e);
       }
   },
   callbackFromNative(callbackId, params, isSuccess) {
       let callback = this.callbacks[callbackId];
       if (callback) {
           if (isSuccess) {
               callback.success && callback.success(callbackId, params);
           } else{ callback.fail && callback.fail(callbackId, params); } delete BDAPPnode.callbacks[callbackId]; }; }};Copy the code

When the request is made, the callback method is registered. This serves two purposes:

  • No need to register all global callback functions in advance, reducing unnecessary initialization and thus reducing white screen time;

  • Instead of calling the name of the callback function, a random ID is passed when the request is made and the callback function with that ID is registered. NA implements the callback logic through the unified package callback function call, callback ID and parameters.

The specific choice of that, but also according to the specific situation specific analysis.

The second kind of interaction model

Request logic: no go, no return, no request, NA active invocation. This class also registers global variables and waits for NA to call. Similar to non-Jsbridge implementations

window.fn1 = () =>{
   // do fn1
}
  
window.fn2 = () =>{
   // do fn2
}

Copy the code

Scheme selection

In the actual combat process, I deeply realized that the mixed development can be divided into two categories: NA service H5, H5 service NA.

The former is mainly H5. Most interaction is that H5 initiates NA request and waits for NA callback, which can be called “one to one request”. For example, H5 requests to obtain geographic location, and NA returns N\S coordinate after completion.

The latter is mainly to solve the problem of high NA cost implementation. It is a good method for NA to proactively call H5 in advance registration, which can be called “separate request” to ensure the smooth implementation of functions.

In the actual project process, there is often this situation: the callback function is not only a one-to-one request, but also a separate call, such as: comment function, you can click the page to pop NA input box to send, you can click the bottom BAR NA button to achieve the box to send. The page needs to be updated. From the perspective of H5, we hope that NA can distinguish between the success of comments called by H5 page and the success of comments called by NA, so that model 1 and model 2 can be separated and implemented independently (at the same time, we can also distinguish the source of page refresh). But from NA’s point of view, it doesn’t matter who did it, as long as the comment is successful, you should call the H5 method on the update page. Otherwise NA needs to carry arguments from the beginning of the call, all the way down. After communication with the end, both sides compromise a step, simple function of source distinction model one implementation, more complex model two implementation.

API package

The API layer is at the bottom of JSBridge and the business, and some people think of it as part of JSBridge, so I’ll separate it out for better understanding. The business layer call is primarily encapsulated here, as shown in the code below.

One more word here: daily development should have the thought of encapsulation and extraction. On the one hand, it reduces the duplication of code, on the other hand, it continuously extracts the code into layers. No layer can do some encapsulation and extension, which can improve the code reuse.

JSB injection timing

NA injection

We certainly expect JSB injection as early as possible, so that it can be called anywhere on the front page at any time, and the methods and timing of NA injection are limited. The following table:

platform methods The timing
IOS[UI] [self.webView stringByEvaluatingJavaScriptFromString:injectjs] WebViewDidFinishLoad (there will be timing issues)
IOS[wk] evaluateJavaScript:xxxx didCreateJavaScriptContext
Android WebView. LoadUrl (” javascript: “+ injectjs);) OnPageFinished

The following methods are used to describe the value of the page state. According to compatibility and implementation integrity, DOMContentLoaded is generally used. In IE9, readyStatechange is used to determine whether the page is loaded successfully.

The name of the Parent object describe compatibility
DOMContentLoaded doc Page content OK IE9+
onload win All pages are loaded as soon as they are complete
readystatechange doc Page loading status: Uninitialized: The object exists but has not been initialized. Loading: The object is loading data. Loaded: Data is loaded to the object. Interactive: The object is ready to be manipulated, but not fully loaded. Complete: The object has been loaded IE9&IE10 have implementation bugs

Uiwebview in IOS provides a proxy WebViewDidFinishLoad, and when WebViewDidFinishLoad is called, readyState may be either interactive or complete, So the initialization page calls directly will have problems. For this problem from the perspective of NA can realize an NSObject extension, and realize the webView: didCreateJavaScriptContext: forFrame. From the H5 perspective, you can detect the page state and call Native after complete.

DidCreateJavaScriptContext of IOS and Android OnPageFinished (the page has finished loading) is completed before the web onload, so these two time didn’t call order.

Advantages:

1) early registration, even in the page initialization on the call end ability, can also meet

Disadvantages:

Since we choose UIWebView according to the above considerations, this approach has several disadvantages: 1) The implementation cost of listening is high; 2) NA injection is required. NA is not familiar with JS, and JS is often not clear about NA logic, so the maintenance cost can not be controlled.

If you don’t have enough time, what else can you do besides inject NA?

JS injection

In fact, JS can also be injected at the beginning of the page. For example, using the Jsbridge code in head directly, we used this downgrading scheme in 8.0 and completed the architecture in a short period of time.

Advantages:

This reduces maintenance costs, improves functionality, and improves the chances of a successful invocation.

Disadvantages:

Added page load parsing time to affect white screen time.

conclusion

Hybrid is an idea to connect H5 and NA, that is, H5 functions can be quickly iterated and NA experience can be achieved. It is a typical development mode of mixed development. In practice, the code implementation needs to be customized according to the business pattern model, and the injection time is not invariable and can be selected according to the business pattern.

reference

JSBridge in mobile hybrid development

Remote procedure call

All the WebView and JS interactions you need are here

UIWebView interacts with WKWebView and JavaScript with OC

How to use UIWebView in iOS

UIWebView code injection timing and posture

Development of Hybrid

The pits used by JavaScriptCore in a real project