JSBridge(Android和IOS平台)的设计和实现
前言
对于商务类的app,随着app注册使用人数递增,app的经营者们就会逐步考虑在应用中展开少量推广活动。大多数活动具有时效性强、经营时间短的特征,一般产品们和经营者们都是通过wap页面快速投放到产品的活动板块。Wap页面可以声文并茂地详情活动,但活动的最终目标是通过获取特权、跳转进入本地功能板块,最后达成交易。如何建立wap页面和本地Native页面的深度交互,这就需要用到本文详情的JSBridge。
此外少量平台类的产品,如大家每天都在使用的微信、支付宝、手机qq等,无一例外都在使用集成JSBridge的webContainer完成众多业务组件功能,大大减少了用户端Native开发的工作量,不仅节约了大量人力开发成本,还能避开产品上线升级的版本审核周期限制(特别是IOS平台)。当然这些超级APP有强大的技术力量支撑,通过JSBridge有计划的进行API规范接口,不断向前台Wap开发人员开放,并在版本上向下兼容。但对于我们刚起步经营的中小级app来说暂时还没有必要如此大张旗鼓,相反前面提到的wap活动推广则是我们的主要需求。
为了满足这个需求,本文通过提炼JSBridge的核心部分改造成JSService方式供各个不同的产品零修改方式使用。各个不同的产品只要要按照插件的方式提供Native扩展接口,并在各自封装的webContainer中调用JSService对Wap调用进行阻拦解决。
具体产品应用
目前该框架同时覆盖了Android和IOS平台,在我司的几个电商类产品中都得到了很好的使用,并趋于稳固。
本文的Demo工程运行效果如下:
image
image
关于JSAPI的接口封装
JSAPI的封装包括核心JS和对外开放接口JS两个部分。 核心JS部分通过阻拦某Q的wap请求页面获取,获取的JS进行编码混淆解决,已经通过调试进行了注释,其主要过程就是对参数和回调进行封装,并构建一个url链接通过创立一个隐藏的iframe进行发送。核心JS代码阅读
对参数和回调进行封装部分的代码如下:
//invoke //mapp.invoke("device", "getDeviceInfo", e); //@param e 类 必需 //@param n 类方法 必需 //@param i 同步回调的js方法 //@param s function k(e, n, i, s) { if (!e || !n) return null; var o, u; i = r.call(arguments, 2), //相当于调用Array.prototype.slice(arguments) == arguments.slice(2),获取argument数组2以后的元素 //令s等于回调函数 s = i.length && i[i.length - 1], s && typeof s == "function" ? i.pop() : typeof s == "undefined" ? i.pop() : s = null, //u为当前存储回调函数的index; u = b(s); //假如当前版本支持Bridge if (C(e, n)) { //将传进来的所有参数生成一个url字符串; o = "ldjsbridge:" + "/" + "/" + encodeURIComponent(e) + "/" + encodeURIComponent(n), i.forEach(function(e, t) { typeof e == "object" && (e = JSON.stringify(e)), t === 0 ? o += "?p=": o += "&p" + t + "=", o += encodeURIComponent(String(e)) }), (o += "#" + u); //带上存储回调的数组index; //执行生成的url, 有些函数是同步执行完毕,直接调用回调函数;而有些函数的调用要通过异步调用执行,需要通过 //全局调用去完成; var f = N(o); if (t.iOS) { f = f ? f.result: null; if (!s) return f; //假如无回调函数,直接返回结果; } }else { console.log("mappapi: the version don't support mapp." + e + "." + n); } }
创立iframe发送JSBridge调用请求:
//创立一个iframe,执行src,供阻拦 function N(n, r) { console.log("logOpenURL:>>" + n); var i = document.createElement("iframe"); i.style.cssText = "display:none;width:0px;height:0px;"; var s = function() { //通过全局执行函数执行回调函数;监听iframe能否加载完毕 E(r, { r: -201, result: "error" }) }; //ios平台,令iframe的src为url,onload函数为全局回调函数 //并将iframe插入到body或者者html的子节点中; t.iOS && (i.onload = s, i.src = n); var o = document.body || document.documentElement; o.appendChild(i), t.android && (i.onload = s, i.src = n); // var u = t.__RETURN_VALUE; //当iframe执行完成之后,最后执行settimeout 0语句 return t.__RETURN_VALUE = e, setTimeout(function() { i.parentNode.removeChild(i) }, 0), u }
对外开放接口的封装:(使用者只要要对该部分进行接口扩展就可)
mapp.build("mapp.device.getDeviceInfo", { iOS: function(e) { return mapp.invoke("device", "getDeviceInfo", e); }, android: function(e) { var t = e; e = function(e) { try { e = JSON.parse(e) } catch(n) {} t && t(e) }, mapp.invoke("device", "getDeviceInfo", e) }, support: { iOS: "1.0", android: "1.0" }}),
核心JS代码调用说明
mapp.version: mappAPI自身版本号mapp.iOS: 假如在ios app中,值为truemapp.android: 假如在android app中,值为truemapp.support: 检查当前app环境能否支持该接口,支持返回true mapp.support("mqq.device.getClientInfo")mapp.callback: 用于生成回调名字,跟着invoke参数传给用户端,供用户端回调 var callbackName = mapp.callback(function(type, index){ console.log("type: " + type + ", index: " + index); });mapp.invoke 方法:mapp核心方法,用于调用用户端接口。 @param {String} namespace 命名空间 @param {String} method 接口名字 @param {Object/String} params 可选,API调用的参数 @param {Function} callback 可选,API调用的回调* 调用普通的无参数接口: mapp.invoke("ns", "method");* 调用有异步回调函数的接口: mapp.invoke("ns", "method", function(data){ console.log(data); }); 或者 mapp.invoke("ns", "method", { "params" : params //参数通过json封装 "callback" : mapp.callback(handler), //生成回调名字 });* 假如有多个参数调用: mapp.invoke("ns", "method", param1, param2 /*,...*/,callback);
JSService的具体实现-插件运行机制
JSService部分是基于Phonegap的Cordova引擎的基础上简化而来,其基本原理参照Cordova的引擎原理如图所示:
image
一般app中都有自己定制的Webcontainer,为了更好的跟已有项目相融合,在Cordova的基础上我们进行了简化,通过JSAPIService服务的方式进行插件扩开展发如图所示:
image
本JSBridge是基于Phonegap的Cordova引擎的基础上简化而来, Android平台Webview和JS的交互方式共有三种:
- ExposedJsApi:js直接调用java对象的方法;(同步)
- 重载chromeClient的prompt 截获方案;(异步)
- url截获+webview.loadUrl回调的方案;(异步)
为了和IOS保持一致的JSAPI,只能选用第三套方案;
基于JSService的插件开发、配置和使用
IOS平台
git地址: Lede-Inc/LDJSBridge_IOS.git
在Native部分,定义一个板块插件对应于创立一个插件类, 板块中的每个插件接口对应插件类中某个方法。
集成LDJSBridge_IOS框架之后,只要要继承框架中的插件基类LDJSPlugin,如下所示:
- 插件接口定义
#import "LDJSPlugin.h" @interface LDPDevice : LDJSPlugin {} //@func 获取设施信息 - (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command; @end
- 自己设置插件接口实现
@implementation LDPDevice/** *@func 获取设施信息 */- (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command{ //读取设施信息 NSMutableDictionary* deviceProperties = [NSMutableDictionary dictionaryWithCapacity:4]; UIDevice* device = [UIDevice currentDevice]; [deviceProperties setObject:[device systemName] forKey:@"systemName"]; [deviceProperties setObject:[device systemVersion] forKey:@"systemVersion"]; [deviceProperties setObject:[device model] forKey:@"model"]; [deviceProperties setObject:[device modelVersion] forKey:@"modelVersion"]; [deviceProperties setObject:[self uniqueAppInstanceIdentifier] forKey:@"identifier"]; LDJSPluginResult* pluginResult = [LDJSPluginResult resultWithStatus:LDJSCommandStatus_OK messageAsDictionary:[NSDictionary dictionaryWithDictionary:deviceProperties]]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];}@end
- 在plugin.json文件中对plugin插件的统一配置
{ "update": "", "module": "mapp", "plugins": [ { "pluginname": "device", "pluginclass": "LDPDevice", "exports": [ { "showmethod": "getDeviceInfo", "realmethod": "getDeviceInfo" } ] } ]}
- 在webContainer中对JSService初始化, 当初始化完成之后,向前台页面发送一个ReadyEvent,前台就可开始调用JSAPI接口;
//注册插件Service if(_bridgeService == nil){ _bridgeService = [[LDJSService alloc] initBridgeServiceWithConfig:@"PluginConfig.json"]; } [_bridgeService connect:_webview Controller:self];/** Called when the webview finishes loading. This stops the activity view. */- (void)webViewDidFinishLoad:(UIWebView*)theWebView{ NSLog(@"Finished load of: %@", theWebView.request.URL); //当webview finish load之后,发event事件通知前台JSBridgeService已经就绪 //监听事件由各个产品自行决定 [_bridgeService readyWithEvent:@"LDJSBridgeServiceReady"];}
Android平台
git地址: Lede-Inc/LDJSBridge_Android.git
- 插件接口定义
public class LDPDevice extends LDJSPlugin { public static final String TAG = "Device"; /** * Constructor. */ public LDPDevice() { } }
- LDJSPlugin 属性方法说明
/** * Plugins must extend this class and override one of the execute methods. */ public class LDJSPlugin { public String id; //在插件初始化的时候,会初始化当前插件所属的webview和controller //供插件方法接口 返回解决结果 public WebView webView; public LDJSActivityInterface activityInterface; //所有自己设置插件需要重载此方法 public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException { return false; } }
- 自己设置插件接口实现
@Override public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException { if (action.equals("getDeviceInfo")) { JSONObject r = new JSONObject(); r.put("uuid", LDPDevice.uuid); r.put("version", this.getOSVersion()); r.put("platform", this.getPlatform()); r.put("model", this.getModel()); callbackContext.success(r); } else { return false; } return true; }
- 在封装的webContainer中注册服务并调用:
/** * 初始化Activity,打开网页,注册插件服务 */ public void initActivity() { //创立webview和显示view createGapView(); createViews(); //注册插件服务 if(jsBridgeService == null){ jsBridgeService = new LDJSService(_webview, this, "PluginConfig.json"); } //加载请求 if(this.url != null && !this.url.equalsIgnoreCase("")){ _webview.loadUrl(this.url); } } /** * 初始化webview,假如需要调用JSAPI,必需为Webview注册WebViewClient和WebChromeClient */ @SuppressLint("SetJavaScriptEnabled") public void createGapView(){ if(_webview == null){ _webview = new WebView(LDPBaseWebViewActivity.this, null); //设置允许webview和javascript交互 _webview.getSettings().setJavaScriptEnabled(true); _webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); //绑定webviewclient _webviewClient = new WebViewClient(){ public void onPageStarted(WebView view, String url, Bitmap favicon){ super.onPageStarted(view, url, favicon); isWebviewStarted = true; } public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); //发送事件通知前台 if(isWebviewStarted){ //在page加载之后,加载核心JS,前台页面可以在document.ready函数中直接调用了; jsBridgeService.onWebPageFinished(); jsBridgeService.readyWithEventName("LDJSBridgeServiceReady"); } isWebviewStarted = false; } @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if(url.startsWith("about:")){ return true; } if(url.startsWith(LDJSService.LDJSBridgeScheme)){ //解决JSBridge特定的Scheme jsBridgeService.handleURLFromWebview(url); return true; } return false; } }; _webview.setWebViewClient(_webviewClient); //绑定chromeClient _webviewChromeClient = new WebChromeClient(){ @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { return super.onJsAlert(view, url, message, result); } }; _webview.setWebChromeClient(_webviewChromeClient); } }
结束
第一次写博客,写得糙和不好的地方望见谅,本人将会不断改善和提高自身能力;所以本博客主要提供大概的处理方案,望能够和有需要的人士交流沟通具体实现方式的差异。
作者:philon
链接:https://www.songma.com/p/90d1bee2f3c9
来源:简书
简书著作权归作者所有,任何形式的转载都请联络作者取得受权并注明出处。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » JSBridge(Android和IOS平台)的设计和实现