首页 > 编程语言 >JavaScript调用App原生代码(iOS、Android)通用解决方案

JavaScript调用App原生代码(iOS、Android)通用解决方案

时间:2022-11-30 10:23:20浏览次数:71  
标签:function resultObj JavaScript iOS callBack result Android null


 

实际场景

场景:现在有一个H5活动页面,上面有一个登陆按钮,要求点击登陆按钮以后,唤出App内部的登录界面,当登录成功以后将用户的手机号返回给H5页面,显示出来。
这个场景应该算是比较完整的一次H5中的JavaScript与App原生代码进行交互了,这个过程,我们制定的方案满足以下几点:

  • 满足基本的交互流程的功能
  • Android与iOS都能适用
  • H5的前端开发者,在书写JavaScript的业务代码的时候不需要为了迁就移动端语言的特性而写特殊的磨合代码
  • 方便调试

交互流程

当H5页面上的JavaScript代码要调用原生的页面或者组件的时候,调用最好是双向的,一来一回,这样比较容易满足一些比较复杂的业务场景,就像上面的场景一样,有调用,有回调告知H5调用的结果。前端开发写的JavaScript代码基本上都是异步风格的,就拿上面的场景,如果登录是H5前端的,那么这个流程就会是:

复制代码
function loginClick() {
    loginComponent.login(function (error,result) {
        //处理登录完成以后的逻辑
    });
}
var loginComponent = {
    callBack:null,
    "login":function (callBack) {
        this.show();
        this.callBack = callBack;
    },
    show:function (loginComponent) {
        //登录组件显示的逻辑
    },
    confirm:function (userName,password) {
        ajax.post('https://xxxx.com/login',function (error,result) {
           if(this.callBack !== null){
                this.callBack(error,result);
           } 
        });
    }
}
复制代码

如果要改成调用原生登录,那么这个流程就应该是这样:

确定了流程,接下来就可以详细设计和实现

原生与JavaScript的桥梁

为了实现上述流程,并且能让H5的前端开发尽可能少的语法损失,我们需要构建一个JavaScript与原生App进行交互的桥梁,这个桥梁来处理与App的协议交互,兼容iOS与Android的交互实现。

Android与iOS都支持在打开H5页面的时候,向H5页面的window对象上注入一个JavaScript可以访问到的对象,Android端使用的是

webView.addJavascriptInterface(myJavaScriptInterface, “bridge”);

iOS则可以使用JavaScriptCore来完成:

复制代码
#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];
复制代码

  这里面Android的myJavaScriptInterface与PICBridge都是作为与JavaScript进行通信的桥梁。
  我们使用设计这个桥梁的时候,需要使用一个具体的语法约定和数据约定,比方说,当前端开发调用App登录的时候,他一定是希望就像调用其他JavaScript的组件一样,而登录的结果通过传入callBack的函数来完成,对于callBack函数,我们希望借助NodeJS的规范:

function(error,res) {
    //回调函数第一个参数是错误,第二个参数是结果
}

以上我们可以看到,bridge必须有能力将前端开发写的JavaScript回调函数传入到App内部,然后App处理完逻辑以后通过回调函数来告知前端处理,并且这个需要通过约定好的数据格式来传递入参和返回值。
为了完成双向通信,我们就需要在JavaScript设置一个bridge,原生再注入一个bridge,这两个bridge按照一定的数据约定来进行双向通信和分发逻辑。

原生端注入到JS当中的“桥”(iOS端)

通过使用JavaScriptCore这个库,我们能很容易的将JavaScript传入的回调函数在objective-c或者是swift端持有,并回去回调这个回调函数。

复制代码
#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;
@end
复制代码

需要说明的是,JavaScript没有函数参数标签的概念,JSExportAs是用来将objective-c的方法映射为JavaScript的函数。
-(void)callRouter:(JSValue )requestObject callBack:(JSValue )callBack);
这个方法是暴露给JavaScript端调用的。
第一个参数requestObject是一个JavaScript对象,传入到objective-c中以后就可以转换为key-value结构的字典,那么这个字典的数据约定是:

{
    'Method':'Login',
    'Data':null
}

其中Method是App内部对外提供的API,而这个Data则是该API需要的入参。
第二个参数是一个callBack函数,该类型的JSValue可以调用callWithArguments:方法来invoke这个回调函数。
前面已经说明,回调函数的第一个参数是error,第二个参数是一个结果,而回调的结果我们也进行一下约定,那就是:

{
    'result':{}
}

这样的好处是,业务逻辑可以讲返回的结果放入result中,跟result同级别的我们还可以加入统一的签名认证的东西,在此暂时不延伸。
原生端的bridge的来实现一下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;
//因为JavaScript是单线程的,需要尽快完成调用逻辑,耗时操作需要异步提交到主线程中执行
        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",@"null"]];
                        }
                    }
                    else{
                        [callBack callWithArguments:@[@"null",@"null"]];
                    }
            } failure:^(NSError *error) {
                    if (error) {
                        [callBack callWithArguments:@[[error description],@"null"]];
                    }
                    else{
                        [callBack callWithArguments:@[@"App Inner Error",@"null"]];
                    }
            }];
        });
    }
    else{

        [callBack callWithArguments:@[@NO,[PICError ErrorWithCode:PICUnkonwError].description]];
    }
    return;
}
//将返回的结果字典转换为字符串通过回调函数传回给JavaScript
-(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{
        return nil;
    }
}
复制代码

callAction函数实际上就是分发业务逻辑用的

复制代码
-(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);
    }
}
复制代码

这个callBack Block是在self.handlers的字典中存储,比较复杂,block第一个参数是传入的入参,后面两个参数是成功以后的回调和失败以后的回调,以便业务逻辑完成后进行回调给JavaScript。
同时会有注册业务逻辑的方法:

复制代码
-(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];
    }
}
复制代码

至此,原生端路由实现完毕。

JavaScript端路由

复制代码
(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 = {

        /**
         *通过bridge调用app端的方法
         * @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()) {
                //生成回调函数方法名称
                var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
                //挂载一个临时函数到window变量上,方便app回调
                win[cbName] = function(err, result) {
                    var resultObj;
                    if (typeof(result) !== 'undefined' && result !== null) {
                        resultObj = JSON.parse(result)['result'];
                    }
                    callback(err, resultObj);
                    //回调成功之后删除挂载到window上的临时函数
                    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;
                    }
                }
            });
        }
    };

    //将mobile对象挂载到window全局
    win.webBridge = mobile;
})(window);
复制代码

在window上挂在一个叫webBridge的对象,其他业务JavaScript可以通过webBridge.login来进行调用原生端开放的API。
callAppRouter方法的实现我们来分析一下:
如果判断是iOS设备,则使用iOS注册的bridge对象进行调用callRouter方法:

复制代码
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);
                });
            }
复制代码

req是标准的包含Method和Data的对象,紧接着传入回调函数,回调函数有err与result,里面做好各种类型检查。
着重说一下Android端的实现,因为Android端的JavaScript方法注册,参数类型只能字符串,java语言本身没有匿名函数的概念,所以只能给Java端传入回调函数的名字,而回调函数的实现则在JavaScript端持有。

复制代码
else if (isAndroid()) {
                //生成回调函数方法名称
                var cbName = 'CB_' + Date.now() + '_' + Math.ceil(Math.random() * 10);
                //挂载一个临时函数到window变量上,方便app回调
                win[cbName] = function(err, result) {
                    var resultObj;
                    if (typeof(result) !== 'undefined' && result !== null) {
                        resultObj = JSON.parse(result)['result'];
                    }
                    callback(err, resultObj);
                    //回调成功之后删除挂载到window上的临时函数
                    delete win[cbName];
                };
                win.bridge.callRouter(JSON.stringify(req), cbName);
            }
复制代码

本质上就是将其他业务JavaScript代码传入的callBack函数通过随机生成函数名,挂在到window变量上,回调以后将其删除:delete win[cbName]。
当调用Java端的bridge.callRouter(JSON.stringify(req), cbName),Java端拿到cbName,在完成业务逻辑后,按照标准数据格式,在JavaScript执行的上下文中,回调这个名字的方法。
至此,前端的webBridge完成。

最后附上Demo地址:
https://github.com/Neojoke/Picidae.git

标签:function,resultObj,JavaScript,iOS,callBack,result,Android,null
From: https://www.cnblogs.com/riverone/p/16937620.html

相关文章

  • Android AIDL相关
    AndroidAIDL相关AIDL是Android接口定义语言(AndroidInterfacedefinitionlanguage)它是一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信(IPC)接......
  • JavaScript合集(流程控制语句)
    流程控制条件判断语句条件分支语句循环语句条件判断语句if语句语法: if(条件表达式){ 语句 } ------- if(a>10){alert('a比10大')......
  • [XState] Create Actor in Vanilla Javascript
     functioncountBehavior(state,event){if(event.type==="INC"){return{...state,count:state.count+1}}}functioncreateA......
  • 23条JavaScript初学者应知的最佳实践方法
    1、优先使用===,而不是==JavaScript使用两种相等性操作符:===|!==和==|!=。通常认为做比较的最佳实践是使用前一组操作符。“若两个操作数的类型和值相同,那么===比较的结果为......
  • 10种经典排序算法的JavaScript实现方法
    排序算法是《数据结构与算法》中最基本的算法之一。常见的一些排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。其中,冒泡排序......
  • 图解实例讲解JavaScript算法,让你彻底搞懂
    你好程序员,我们大多数人都害怕算法,并且从未开始学习它。但我们不应该害怕它。算法只是解决问题的步骤。今天让我们以简单和说明性的方式介绍主要算法。不要试图记住它们......
  • Unity用户手册-Unity与Android、iOS互相调用
        C#是以Assembly(汇编集)为一个基本单位组织代码的,dll就是一个assemble,dll之间有加载依赖顺序。dll是windows平台上的动态库,而so是linux平台上的动态库,最后.a是IOS......
  • 一行能装逼的JavaScript代码
    一行神奇的js代码,当时我就震惊了,这不就是传说中的ZB神奇么……哈哈。写本篇文章的缘由是之前看到了一段js代码,如下:​​(!(~+[])+{})[--[~+""][+[]]*[~+[]]+~~!+[]]+({}+......
  • 教你用JavaScript实现随机点名
    案例介绍欢迎来到我的小院,我是霍大侠,恭喜你今天又要进步一点点了!我们来用JavaScript相关知识,做一个随机点名的案例。你可以通过点击开始按钮控制上方名字的闪动,点击停止......
  • IOS真机自动化操作环境搭建
    环境版本IMAC:Ventura13.0.1(当前最新)XCODE:Version14.1(当前最新)Iphone7:15.7.1iTunes:windows6412.12.6.1(当前最新)基本架构1、手机端的WDARunner(We......