实际场景

场景:现在有一个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. iOS - 切换图片/clip subview/iCarousel

    源代码:点击打开链接 这个图片展示的时候,我只想让它在蓝色的view上展示..就需要去设置view的一个属性clip subview..把这个属性打勾...view有个clip subview属性,选 ...

  2. 写出优质Java代码的4个技巧(转)

    http://geek.csdn.net/news/detail/238243 原文:4 More Techniques for Writing Better Java 作者:Justin Alban ...

  3. [Functional Programming] Arrow contramap vs map and promap

    In previous post, Arrow Functor with contramap, we have seen how to opreating on params before we in ...

  4. ASP入门(十三)-Server对象

    Server 对象用于处理服务器上的一些特殊任务,例如,创建组件实例.获取文件路径.执行ASP脚本文件等. Server 对象是体现 ASP 强大功能的一个对象,之前介绍的对象都是针对获取.请求以及简 ...

  5. js获取当月最后一天

    构造函数 new Date(); new Date(value); new Date(dateString); new Date(year, month[, day[, hour[, minutes[ ...

  6. OpenWRT下实现Portal认证(WEB认证)

    首先简单介绍一下什么是Portal认证,Portal认证,通常也会叫Web认证,未认证用户上网时,设备强制用户登录到特定站点,用户可以免费访问其中的服务.当用户需要使用互联网中的其它信息时,必须在门户 ...

  7. android自己定义进度值可拖动的seekbar

    近期忙找实习,加上实验室在推新项目,须要学习新知识.所以非常长一段时间没去整理了官博客了,github也蛮久没更新.非常羞愧.接下来还是要坚持写. 今天就简单的写一下我在项目中用到的算自己定义seek ...

  8. Asp.NET MVC 之 调试访问 webservice 时出现“ 无法找到资源 ”的错误

    问题情景如标题,具体错误如下图: 出现以上情况,是程序将 .asmx 文件按控制器方式解析了,在 RouteConfig.cs 文件的 RegisterRoutes 方法中忽略 .asmx 文件,&q ...

  9. 微软BI 之SSAS 系列 - 多维数据集中度量值设计时的聚合函数 (累加性_半累加性和非累加性)

    在 SSAS 系列 - 实现第一个 Cube 以及角色扮演维度,度量值格式化和计算成员的创建 中主要是通过已存在的维度和事实数据创建了一个多维数据集,并同时解释了 Role-Playing Dimen ...

  10. vsphere 5.1 改进和SSO理解

    虚拟交换器 以5.1版的vSphere而言,VMware在VDS上提供一些新功能.例如,现在可以用快照的方式,来备份还原VDS组态及网络端口群组(port group)的组态,以因应vCenter S ...