In the past, I wrote an article about H5 and App native interaction scheme. Many people asked if there was any example code. Today, HERE is a general code practice for iOS and Android

The actual scene

Scenario: Now there is an H5 active page with a login button on it. After clicking the login button, the login interface inside the App will be called out. When the login succeeds, the user’s mobile phone number will be returned to the H5 page and displayed. This scene should be a relatively complete interaction between JavaScript and App native code in H5. In this process, the scheme we developed meets the following points:

  1. Functions that satisfy basic interaction processes
  2. It works on both Android and iOS
  3. H5 front-end developers do not need to write special run-in code to accommodate the features of the mobile language when writing JavaScript business code
  4. In terms of debugging

The interaction process

As mentioned in the previous article, when JavaScript code on an H5 page calls a native page or component, it is best to make the call in both directions, once and for all. This makes it easier to satisfy complex business scenarios, such as the above scenario, where there are calls and callbacks to inform H5 of the result of the call. Front-end development writing JavaScript code is basically asynchronous style, take the above scenario, if the login is H5 front-end, then the flow would be:

The code is as follows:

function loginClick() {
    loginComponent.login(function (error,result) {
        // Handle logic after the login is complete
    });
}
var loginComponent = {
    callBack:null."login":function (callBack) {
        this.show();
        this.callBack = callBack;
    },
    show:function (loginComponent) {
        // The logic displayed by the login component
    },
    confirm:function (userName,password) {
        ajax.post('https://xxxx.com/login'.function (error,result) {
           if(this.callBack ! = =null) {this.callBack(error,result); }}); }}Copy the code

If you were to invoke native login instead, the flow would look like this:


Bridge between native and JavaScript

In order to realize the above process and make the front-end development of H5 with as little syntax loss as possible, we need to build a bridge for interaction between JavaScript and native App, which can deal with the protocol interaction with App and be compatible with the interaction implementation of iOS and Android.

Both Android and iOS support the injection of an object accessible by JavaScript into the window object of the H5 page when opening the H5 page

WebView. AddJavascriptInterface (myJavaScriptInterface, "bridge");Copy the code

For iOS, JavaScriptCore can be used to do this:

#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PICBridgeExport <JSExport>
@end
@interface PICBridge : NSObject<PICBridgeExport>
@end


self.jsContext =  [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.bridge =[[PICBridge alloc]init];Copy the code

Android’s myJavaScriptInterface and PICBridge are used as Bridges to communicate with JavaScript. When we use the design bridge, we need to use a specific syntax and data conventions. For example, when the front-end developer calls App login, it must hope that it is just like calling other JavaScript components, and the result of login is completed through the function passed in callBack. For the callBack function, We want to use the NodeJS specification:

function(error,res) {
    // The first argument to the callback is an error and the second argument is the result
}Copy the code

As you can see above, the Bridge must have the ability to pass JavaScript callbacks written by the front-end development into the App, which then informs the front-end when the App has finished processing the logic, and this needs to be passed in the agreed data format for the input parameters and return values. To achieve two-way communication, we need to set up a bridge in JavaScript and natively inject a bridge, which performs two-way communication and distribution logic according to certain data conventions.

“Bridge” for native injection into JS (iOS)

By using the JavaScriptCore library, it is easy to hold the callback passed in by JavaScript in Objective-C or swift and call it back.

#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol PICBridgeExport <JSExport>
JSExportAs(callRouter, -(void)callRouter:(JSValue *)requestObject callBack:(JSValue *)callBack);
@end
@interface PICBridge : NSObject<PICBridgeExport>
-(void)addActionHandler:(NSString *)actionHandlerName forCallBack:(void(^)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)))callBack;
@endCopy the code

It should be noted that JavaScript has no concept of function argument tags, and JSExportAs is a function used to map Objective-C methods to JavaScript. -(void)callRouter:(JSValue )requestObject callBack:(JSValue )callBack); This method is exposed to JavaScript side calls. The first parameter requestObject is a JavaScript object that can be converted to a key-value dictionary in Objective-C. The data convention for this dictionary is:

{
    'Method':'Login'.'Data':null
}Copy the code

Method is the API provided by App, and Data is the input parameter required by the API. The second argument is a callBack function. The JSValue of this type can invoke the callBack function by calling callWithArguments:. The first argument to the callback function is error, and the second argument is a result.

{
    'result':{}
}Copy the code

The advantage of this is that the business logic can put the returned result into result, and we can also add uniform signature authentication at the same level as result, which will not be extended here. Bridge of native end to implement callRouter:

- (void)callRouter:(JSValue *)requestObject callBack:(JSValue *)callBack{
    NSDictionary * dict = [requestObject toDictionary];
    NSString * methodName = [dict objectForKey:@"Method"];
    if(methodName ! = nil && methodName.length>0) {
        NSDictionary * params = [dict objectForKey:@"Data"];
        __weak PICBridge * weakSelf = self;
// Because JavaScript is single-threaded, the calling logic needs to be completed as quickly as possible, and time-consuming operations need to be committed asynchronously to the main thread for execution
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf callAction:methodName params:params success:^(NSDictionary *responseDict) {
                    if(responseDict ! = nil) { NSString * result = [weakSelf responseStringWith:responseDict];if (result) {
                            [callBack callWithArguments:@[@"null",result]];
                        }
                        else{
                            [callBack callWithArguments:@[@"null"The @"null"]]. }}else{
                        [callBack callWithArguments:@[@"null"The @"null"]];
                    }
            } failure:^(NSError *error) {
                    if (error) {
                        [callBack callWithArguments:@[[error description],@"null"]];
                    }
                    else{
                        [callBack callWithArguments:@[@"App Inner Error"The @"null"]]. }}]; }); }else{

        [callBack callWithArguments:@[@NO,[PICError ErrorWithCode:PICUnkonwError].description]];
    }
    return;
}
// Converts the returned dictionary into a string passed back to JavaScript via the callback function
-(NSString *)responseStringWith:(NSDictionary *)responseDict{
    if (responseDict) {
        NSDictionary * dict = @{@"result":responseDict};
        NSData * data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        NSString * result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        return result;
    }
    else{
        returnnil; }}Copy the code

The callAction function is actually used to distribute the business logic

- (void)callAction:(NSString *)actionName params:(NSDictionary *)params success:(void(^)(NSDictionary * responseDict))success failure:(void(^)(NSError * error))failure{
    void(^callBack)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)) = [self.handlers objectForKey:actionName];
    if (callBack != nil) {
        callBack(params,failure,success);
    }
}Copy the code

The first argument is an incoming argument, followed by a success callBack and a failure callBack to JavaScript after the business logic has completed. There is also a way to register business logic:

- (void)addActionHandler:(NSString *)actionHandlerName forCallBack:(void(^)(NSDictionary * params,void(^errorCallBack)(NSError * error),void(^successCallBack)(NSDictionary * responseDict)))callBack{
    if (actionHandlerName.length>0 && callBack != nil) {
        [self.handlers setObject:callBack forKey:actionHandlerName];
    }
}Copy the code

At this point, the implementation of the native route is complete.

JavaScript client routing

Paste the full code first:

(function(win) {

    var ua = navigator.userAgent;
    function getQueryString(name) {
        var reg = new RegExp('(^ | &)' + name + '= (/ ^ & *) (& | $)'.'i');
        var r = window.location.search.substr(1).match(reg);
        if(r ! = =null) return unescape(r[2]);
        return null;
    }

    function isAndroid() {
        return ua.indexOf('Android') > 0;
    }

    function isIOS() {
        return /(iPhone|iPad|iPod)/i.test(ua);
    }
    var mobile = {

        /** * Call the appside method via bridge * @param method * @param params * @param callback */
        callAppRouter: function(method, params, callback) {
            var req = {
                'Method': method,
                'Data': params
            };
            if (isIOS()) {
                win.bridge.callRouter(req, function(err, result) {
                    var resultObj = null;
                    var errorMsg = null;
                    if (typeof(result) ! = ='undefined'&& result ! = ='null'&& result ! = =null) {
                        resultObj = JSON.parse(result);
                        if (resultObj) {
                            resultObj = resultObj['result']; }}if(err ! = ='null' && typeof(err) ! = ='undefined'&& err ! = =null) {
                        errorMsg = err;
                    }
                    callback(err, resultObj);
                });
            } else if (isAndroid()) {
                // Generate the callback method name
                var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
                // Mount a temporary function to the window variable to facilitate app callbacks
                win[cbName] = function(err, result) {
                    var resultObj;
                    if (typeof(result) ! = ='undefined'&& result ! = =null) {
                        resultObj = JSON.parse(result)['result'];
                    }
                    callback(err, resultObj);
                    // Delete temporary functions mounted on window after successful callback
                    delete win[cbName];
                };
                win.bridge.callRouter(JSON.stringify(req), cbName); }},login: function() {
            // body...
            this.callAppRouter('Login'.null.function(errMsg, res) {
                // body...

                if(errMsg ! = =null&& errMsg ! = ='undefined'&& errMsg ! = ='null') {}else {
                    var name = res['phone'];
                    if(name ! = ='undefined'&& name ! = ='null') {
                        var button = document.getElementById('loginButton'); button.innerHTML = name; }}}); }};// Mount the mobile object to the window globalwin.webBridge = mobile; }) (window);Copy the code

Hang an object called webBridge on the window, and other business JavaScript can use webBridge. Login to call the native open API. CallAppRouter (iOS); callAppRouter (iOS);

if (isIOS()) {
                win.bridge.callRouter(req, function(err, result) {
                    var resultObj = null;
                    var errorMsg = null;
                    if (typeof(result) ! = ='undefined'&& result ! = ='null'&& result ! = =null) {
                        resultObj = JSON.parse(result);
                        if (resultObj) {
                            resultObj = resultObj['result']; }}if(err ! = ='null' && typeof(err) ! = ='undefined'&& err ! = =null) {
                        errorMsg = err;
                    }
                    callback(err, resultObj);
                });
            }Copy the code

Req is a standard object that contains Method and Data, followed by a callback function called err and result that does all the type checking. The Java language itself does not have the concept of anonymous functions, so it can only pass the name of the callback function to the Java side. The callback function implementation is held on the JavaScript side.

else if (isAndroid()) {
                // Generate the callback method name
                var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
                // Mount a temporary function to the window variable to facilitate app callbacks
                win[cbName] = function(err, result) {
                    var resultObj;
                    if (typeof(result) ! = ='undefined'&& result ! = =null) {
                        resultObj = JSON.parse(result)['result'];
                    }
                    callback(err, resultObj);
                    // Delete temporary functions mounted on window after successful callback
                    delete win[cbName];
                };
                win.bridge.callRouter(JSON.stringify(req), cbName);
            }Copy the code

In essence, the callBack function passed in by other business JavaScript code is randomly generated and attached to the window variable, which will be deleted after the callBack: delete win[cbName]. When the Java side calls bridge.callRouter(json.stringify (req), cbName), the Java side gets the cbName and, after the business logic is complete, calls back the method with that name in the context of JavaScript execution, according to the standard data format. At this point, the front-end webBridge is complete.

Finally, attach the Demo address: github.com/Neojoke/Pic… This Demo calls the native login interface through H5. After successful login, the login button of the mobile phone number on H5 will be displayed to complete a set of logical interaction. Like to a ✨, there are any problems we communicate!