实际场景

场景:现在有一个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>) {
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> && 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().match(reg);
if (r !== null) return unescape(r[]);
return null;
} function isAndroid() {
return ua.indexOf('Android') > ;
} 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() * );
//挂载一个临时函数到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() * );
//挂载一个临时函数到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

JavaScript调用App原生代码(iOS、Android)通用解决方案的更多相关文章

  1. C#/IOS/Android通用加密解密方法

    原文:C#/IOS/Android通用加密解密方法 公司在做移动端ios/android,服务器提供接口使用的.net,用到加密解密这一块,也在网上找了一些方法,有些是.net加密了android解密 ...

  2. 封装 React Native 原生组件(iOS / Android)

    封装 React Native 原生组件(iOS / Android) 在 React Native中,有很多种丰富的组件了,例如 ScrollView.FlatList.SectionList.Bu ...

  3. 使用JavaScript调用aspx后台代码

    方法1:js同步调用 触发: onclick="javascript:share('<%# Eval("id_File") %>')" 页面函数: ...

  4. Hybrid App开发模式中, IOS/Android 和 JavaScript相互调用方式

    IOS:Objective-C 和 JavaScript 的相互调用 iOS7以前,iOS SDK 并没有原生提供 js 调用 native 代码的 API.但是 UIWebView 的一个 dele ...

  5. 【转】NativeScript的工作原理:用JavaScript调用原生API实现跨平台

    原文:https://blog.csdn.net/qq_21298703/article/details/44982547 -------------------------------------- ...

  6. .NET/android/java/iOS AES通用加密解密

    移动端越来越火了,我们在开发过程中,总会碰到要和移动端打交道的场景,比如.NET和android或者iOS的打交道.为了让数据交互更安全,我们需要对数据进行加密传输.今天研究了一下,把几种语言的加密都 ...

  7. JS与APP原生控件交互

    "热更新"."热部署"相信对于混合式开发的童鞋一定不陌生,那么APP怎么避免每次升级都要在APP应用商店发布呢?这里就用到了混合式开发的概念,对于电商网站尤其显 ...

  8. PhoneGap或者Cordova框架下实现Html5中JS调用Android原生代码

    PhoneGap或者Cordova框架下实现Html5中JS调用Android原生代码 看看新闻网>看引擎>开源产品 0人收藏此文章, 发表于8小时前(2013-09-06 00:39) ...

  9. 如何实现 javascript “同步”调用 app 代码

    在 App 混合开发中,app 层向 js 层提供接口有两种方式,一种是同步接口,一种一异步接口(不清楚什么是同步的请看这里的讨论).为了保证 web 流畅,大部分时候,我们应该使用异步接口,但是某些 ...

随机推荐

  1. Android -- EventBus使用

    EventBus EventBus是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间.组件与后台线程间的通信.比如请求网络,等网络返回时通过Handler ...

  2. (转)NGUI中深度depth和z轴关系

    先列出转载链接: http://game.ceeger.com/forum/read.php?tid=8917 转载原文: 问题源自一个帖子,因为上传的图比较多,就另开了这个贴写下自己的试验结果,原帖 ...

  3. ZooKeeper启动报错 JAVA_HOME is incorrectly set

    解决办法:在zkEnv.cmd文件中直接写死调用的jdk路径 set JAVA_HOME="D:\Program Files\Java7\jdk1.7.0_51" if not e ...

  4. [Spring boot] Autowired by name, by @Primary or by @Qualifier

    In the example we have currently: @Component public class BinarySearchImpl { @Autowired private Sort ...

  5. Install SVN (Subversion) Server on Fedora 20/19, CentOS/Red Hat (RHEL) 6.5/5.10

    Install SVN (Subversion) Server on Fedora 20/19, CentOS/Red Hat (RHEL) 6.5/5.10 Updated by JR on Mar ...

  6. ArcGIS 10.4的0x80040228许可错误

    今天,再次遇到这个问题,再忙得把它记录下来! AO/AE程序的许可方式 不管是开发环境是基于ArcObject还是基于ArcEngine,不管运行环境是Desktop还是Runtime(早已改名了为A ...

  7. javascript获取和设置URL中的参数

    勘误版 function getQuery(key, url) { url = url || window.location.href; if (url.indexOf('#') !== -1) ur ...

  8. 搭建持续集成单元测试平台(Jenkins+Ant+Java+Junit+SVN)

    一.环境准备 Jenkins: 到官网下载jenkins.war包:http://jenkins-ci.org/ 安装方法有两种: 把下载下来的jenkins.war包放到文件夹下,如C:\jenki ...

  9. 微软BI 之SSRS 系列 - 在 Cube 中通过 MDX 查询实现基于父子递归关系的汇总报表

    之前我写了一篇在 SSRS 开发中处理这种父子关系的汇总与聚合的文章 (SSRS 系列 - 使用分组 Group 属性实现基于父子递归关系的汇总报表),示例中的查询是基于 SQL Server 关系型 ...

  10. HDOJ 5288 OO’s Sequence 水

    预处理出每一个数字的左右两边能够整除它的近期的数的位置 OO's Sequence Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 13 ...