实际场景

场景:现在有一个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. ICTCLAS中的HMM人名识别

    http://www.hankcs.com/nlp/segment/ictclas-the-hmm-name-recognition.html 本文主要从代码的角度分析标注过程中的细节,理论谁都能说, ...

  2. Java开发之富文本编辑器TinyMCE

    一.题外话 最近负责了一个cms网站的运维,里面存在很多和编辑器有关的问题,比如编辑一些新闻博客,论文模块.系统采用的是FCKEditor,自我感觉不是很好,如下图 特别是在用户想插入一个图片的话,就 ...

  3. java多线程分块上传并支持断点续传最新修正完整版本[转]

    package com.test; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.Fi ...

  4. .NET 服务器定位模式(Service Locator Pattern)——Common Service Locator

    本文内容 场景 目标 解决方案 实现细节 思考 相关模式 更多信息 参考资料 Common Service Locator 代码很简单,它一般不会单独使用,而是作为一个单件模式,与像 .net Uni ...

  5. js escape 与php escape

    javascript有编码函数escape()和对应的解码函数unescape(),而php中只有个urlencode和urldecode,这个编码和解码函数对encodeURI和encodeURIC ...

  6. 关于ngModelOptions用法总结 让校验不过的验证绑定ngModel

    updataOn 指定ng-model以什么绑定事件触发 default 就是默认的大家都知道blur 失去焦点的时候更新mouseover 鼠标滑过....... <input type=&q ...

  7. 图解:如何在LINUX中安装VM-Tools

    转自:http://blog.csdn.net/fu9958/article/details/4807000 使用VM安装虚拟系统,真的很方便.可以让个人轻松拥有一个网络,并包含有很多中系统. 因此, ...

  8. Yahoo邮箱最后登录,成为历史!

  9. UBUNTU 字符界面来回切换

    图形界面切换到字符界面: 实体机:Ctrl + Alt + F1 VMware虚拟机:按下ALT+CTRL+SPACE(空格),ALT+CTRL不松开,再按F1.这样就可以切换到字符界面 字符界面切换 ...

  10. SpringMVC学习笔记三:拦截器

    一:拦截器工作原理 类比Struts2的拦截器,通过拦截器可以实现在调用controller的方法前.后进行一些操作. 二:拦截器实现 1:实现拦截器类 实现HandlerInterceptor 接口 ...