Hybrid开发中JSBridge的实现
前言
大前端这个概念可能在不同的公司中定义不同,目前在不少公司比较强调大前端的概念,在大前端中包含了前端与客户端(ios、android),对技术广度有一定的要求。而在Hybrid的开发中,往往需要前端和客户端的同学进行配合,规范一些约定来实现前端html和客户端的通信。
目前很多公司的前端,在面对客户端同学要进行技术对接时候,不知道该怎么去对接,此文将站在前端开发的视角去详细介绍各端开发的原理,并普及前端需要在Hybrid开发中扮演的角色。
JSBridge在Hybrid开发中的位置
目前市场上的应用大部分都是进行Hybrid开发,而开源的库也不少,例如:PhoneGap、Codova、HBuilder等。这些开源库的jssdk中都包含在JSBridge的代码。
JSBridge在Hybrid开发中的作用 :
正如上图所示,JSBridge是连接Native(客户端)和JavaScript前端的桥梁,通过JSBridge 两端的代码才可以通信。
简单的说,JSBridge 一方面给js提供了调用native的方法,而反过来,它也承接了native调用js事件队列的封装。JSBridge构建了js和native之间的通信,而且是双向的。
一般JSBridge中实现的通用功能
- 自定义titleBar
- 自定义titleBar上左右两侧按钮的功能及样式
- 打开一个新的webview来承接跳转的url
- 关闭自身webview
- 关闭前n个webview
- 监听resume、pause事件
- 下拉刷新
- app唤起
JSBridge中实现的业务功能
- 页面分享(微信、微博分享)
- 登录SDK页面呼启
- 支付功能
- 调用相机、图片上传等
- 定位信息获取
JSBridge的开发,需要哪些知识储备
- scheme协议是什么
- 可以简单理解为自定义的url
- 形式如:
[scheme:][//domain][path][?query][#fragment]
- 举个栗子:
jsbridge://openPage?url=https%3A%2F%2Fwww.baidu.com
- js调用native的方法
- native通过拦截约定好的scheme协议 去执行一些native的方法
- 约定固定格式的scheme协议,例如:
[customscheme:][//methodName][?params={data, callback}]
- customscheme:自定义需要拦截的scheme
- methodName:需要调用的native的方法
- params:传递给native的参数 和 回调函数名
- 通过在webview中挂载到全局对象上的对象方法来调用native的方法
- 举个栗子:
window.JSBridge.openPage("https://www.baidu.com")
- native注入到webview全局对象为JSBridge,通过全局对象JSBridge 可以调用挂载在其上的方法,来触发调用native的方法
- native还会拦截 js调用的alert、confirm、prompt方法,通过在js里使用这三个方法 也能进行js对native的通信
js调用native方法 ios代码示例
//=========================全局对象调用postMessage==========================
//js操作native的方法 拦截
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSDictionary *msgMap = [NSDictionary dictionaryWithDictionary:message.body];
NSLog(@"JS交互参数:%@", msgMap);
NSLog(@"name:%@", message.name);
NSString *nativeMethod = msgMap[@"nativeMethod"];
NSLog(@"nativeMethod:%@", nativeMethod);
if([nativeMethod isEqualToString:@"openPage"]){ //打开新的webview页面
CustomWebViewController *customController = [[CustomWebViewController alloc] init];
NSString *url = [self getValueForParams:@"url" forParams:msgMap[@"params"]];
if(url){
customController.url = url;
}
[self.navigationController pushViewController:customController animated:YES];
} else if([nativeMethod isEqualToString:@"hideTitle"]) { //隐藏titlebar
self.navigationController.navigationBarHidden = YES;
} else if([nativeMethod isEqualToString:@"showTitle"]) { //显示titlebar
self.navigationController.navigationBarHidden = NO;
} else if([nativeMethod isEqualToString:@"popPage"]) { //关闭webview
int step = 1;
id s = [self getValueForParams:@"step" forParams:msgMap[@"params"]];
if(s){
step = [s intValue];
}
int totals = self.navigationController.viewControllers.count;
int targetIndex = totals - step - 1;
if(targetIndex >= 0){
UIViewController *targetController = self.navigationController.viewControllers[targetIndex];
[self.navigationController popToViewController:targetController animated:YES];
} else {
[self.navigationController popViewControllerAnimated:YES];
}
} else if([nativeMethod isEqualToString:@"addResumeEvent"]) { //resume监听 进入前台
NSString *js = [NSString stringWithFormat:@"JSBridge.eventMap.%@()", msgMap[@"callback"]];
[_resumeQueue addObject:js];
} else if([nativeMethod isEqualToString:@"addPauseEvent"]) { //pause监听 压入后台
NSString *js = [NSString stringWithFormat:@"JSBridge.eventMap.%@()", msgMap[@"callback"]];
// [_customWebView evaluateJavaScript:js completionHandler:nil]; //todo
[_resumeQueue addObject:js];
} else {
return ;
}
}
//=========================alert拦截==========================
//alert实现 webview会对js进行 alert等系统事件的拦截
- (void) webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:NULL];
}
- native调用js的方法
- ios可以通过webview的
evaluateJavaScript:completionHandler
方法来运行js的代码 - android可以通过webview的
loadUrl()
去调用js代码,也可以使用evaluateJavascript()
来调用js代码 - android中
evaluateJavascript
的使用比loadUrl更高效 也更简洁,唯一不好的是 需要android 4.4以上的版本支持
native调用js方法 ios代码示例
[_customWebView evaluateJavaScript:[@"alert('Hello JSBridge')"] completionHandler:nil];
//resume方法, app进入前台
- (void)resumeEvents{
NSLog(@"==============resumeEvents==============");
//循环遍历执行注册的resume方法
for(int i = 0; i < [_resumeQueue count]; i++){
//native调用js方法
[_customWebView evaluateJavaScript:[_resumeQueue objectAtIndex:i] completionHandler:nil];
}
}
- 全局对象方法定义需要注意哪些
- 在native的开发中,开发者可以给webview注入全局变量并挂载在window对象上,这样前端js就可以通过window上全局对象方法 来调用一些native的方法
- 前端需要去了解这个全局对象,是在webview初始化时候注入的,还是在页面加载完之后注入的,也就是同步注入还是异步注入的问题
- 如果是异步注入的,则需要前端的代码中,添加对象的ready监听机制
怎么开发JSBridge的前端部分
- 1、和native同学约定统一固定格式的通信方式(ios 和 android尽量保持一致,如果不一致 也请各自端内保持一致)
- 2、为JSBridge添加订阅者模式事件
- 主要解决的问题为,js调用native方法的callback回调执行问题
- 这个功能和JSONP的原理很像:预先定义一个全局函数,然后把函数名传递给native,native执行完业务逻辑之后,再去执行调用从前端拿到的全局函数 ---->"callback(data)"
- 3、通过UA信息,获取所在容器信息和系统信息,然后再分别调用所属系统app的方法
- 4、开发兜底程序方案,即非native环境下的js兼容代码
- 示例:https://github.com/q946401639/Hybrid-Android/blob/master/app/src/main/assets/JSBridgeDemo.html
var JSBridge = {
isJSBridgeAPPDemo: isJSBridgeAPPDemo(),//return bool
device: getDeviceInfo(),//return type:ios
eventMap: {
//事件队列
},
uid: 0,
deviceRouter: function(method, params, callback, isHold){
if(!this.isJSBridgeAPPDemo){ return console.log("isJSBridgeAPPDemo:false"); }
if(this.device.type == IOS){
this.iosMethod(method, params, callback, isHold);
} else if (this.device.type == ANDROID){
this.androidMethod(method, params, callback, isHold);
} else {
console.error("请在native端使用此方法:" + method);
}
},
iosMethod: function(method, params, callback, isHold){ //ios通用方法
var self = this;
var uid = this.uid;
if(callback){
this.eventMap[method + uid] = function(data){
callback && callback(data);
if(!isHold){
delete self.eventMap[method + uid];
}
};
this.uid = uid + 1;
}
try{
var msgObj = {
nativeMethod: method
};
if(callback){
msgObj.callback = method + uid;
}
if(params){
msgObj.params = JSON.stringify(params);
}
window.webkit.messageHandlers.JSBridge.postMessage(msgObj);
}catch(e){
console.error(JSON.stringify(e));
console.error("需要在ios设备内使用WKWebView");
}
},
androidMethod: function(method, params, callback, isHold){ //android通用方法
var self = this;
var uid = this.uid;
if(callback){
this.eventMap[method + uid] = function(data){
callback && callback(data);
if(!isHold){
delete self.eventMap[method + uid];
}
};
this.uid = uid + 1;
}
try{
if(params && callback){
JSBridgeAndroid[method](JSON.stringify(params), method + uid);
} else if(params) {
JSBridgeAndroid[method](JSON.stringify(params));
} else if(callback) {
JSBridgeAndroid[method](method + uid);
} else {
JSBridgeAndroid[method]();
}
}catch(e){
console.error(JSON.stringify(e));
console.error("需要在android设备内使用JSBridge功能,无全局对象JSBridgeAndroid");
}
},
getDeviceId: function(callback){ //获取设备id
this.deviceRouter("getDeviceId", null, callback);
},
openPage: function(url){ //打开新的webview
this.deviceRouter("openPage", {url: url}, null);
if(!this.isJSBridgeAPPDemo){
location.href = url;
}
},
popPage: function(n){ //关闭页面
if(!n) { n = 1 }
this.deviceRouter("popPage", {step: n}, null);
if(!this.isJSBridgeAPPDemo){
if(n == 1){
history.back();
} else {
history.go(-n);
}
}
// JSBridgeAndroid.popPage(n);
},
hideTitle: function(){ //隐藏native title bar
this.deviceRouter("hideTitle", null, null);
// JSBridgeAndroid.hideTitle();
},
showTitle: function(){ //显示native title bar
this.deviceRouter("showTitle", null, null);
// JSBridgeAndroid.showTitle();
},
addResumeEvent: function(callback){ //resume
this.deviceRouter("addResumeEvent", null, callback, true);
if(!this.isJSBridgeAPPDemo){
pageBackFromNextPage(callback);
}
// JSBridgeAndroid.addResumeEvent(name + uid);
},
addPauseEvent: function(callback){ //pause
this.deviceRouter("addPauseEvent", null, callback, true);
if(!this.isJSBridgeAPPDemo){
pagePause(callback);
}
}
};
var J_GotoBaidu = document.getElementById("J_GotoBaidu");
J_GotoBaidu.onclick = function(){
JSBridge.openPage("https://www.baidu.com");
};
- 下面两个示例中JSBridge的部分 代码相同,为兼容三端的代码
- ios demo示例
- android demo示例
- 关于上面两个app示例的代码分析,可以查看两示例的readme文件,还有具体代码中的注释也是很详细的
JSBridge的调用需要注意哪些事项
- scheme的请求 不要使用location.href
- 如果webview为对scheme进行拦截,很可能会出现webview报错现象,原因是webview把自定义的scheme协议当正常的url去加载了
- 解决办法是在页面上添加一个iframe,给iframe的src赋值为自定义scheme,这样也能发送这个scheme请求
//ios自定义协议拦截
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
NSURL *url = navigationAction.request.URL;
NSString *scheme = [url scheme];
//ios接收到的scheme的值 始终为小写
if([scheme isEqualToString:@"jsbridge"]){
[self handleCustomAction:url];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
//自定义执行的 协议拦截方法
- (void)handleCustomAction:(NSURL *)url{
NSString *host = [url host];
NSLog(@"host: %@", host);
NSLog(@"params: %@", [url query]);
//解析query为字典
NSArray *paramsArray = [[url query] componentsSeparatedByString:@"&"];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
for(int i = 0; i < [paramsArray count]; i++){
NSArray *step = [paramsArray[i] componentsSeparatedByString:@"="];
[params setObject:step[1] forKey:step[0]];
}
if([host isEqualToString:@"openPage"]){
NSString *pageUrl = params[@"url"];
pageUrl = [pageUrl stringByRemovingPercentEncoding];
CustomWebViewController *customController = [[CustomWebViewController alloc] init];
customController.url = pageUrl;
[self.navigationController pushViewController:customController animated:YES];
}
}
- ios根据使用的webview控件的不同,对于全局对象注入的方式也是不同
- ios开发自带两种webview控件
- UIWebview(ios8 以前的版本,建议弃用)
- 版本较老
- 可使用JavaScriptCore来注入全局自定义对象
- 占用内存大,加载速度慢
- WKWebview
- 版本较新
- 加载速度快,占用内存小
- js使用全局对象window.webkit.messageHandlers.{NAME}.postMessage 来调用native的方法
- fed需要汇总两端的定义 并进行封装
- 需要让ios和android开发同学,制定统一的规范,减少前端写过多的ua判断,以避免不必要的错误
- h5唤起app
- scheme约定应全部为小写,android机器不识别大写的scheme
- 上面两个示例app中 用h5唤起的并跳转至百度的方式为
- IOS:jsbridgedemo://openPage?url=https%3A%2F%http://2Fwww.baidu.com
- Android:
- jsbridgedemo://openPage?url=https%3A%2F%http://2Fwww.baidu.com
- intent://openPage?url=https%3A%2F%2Fwww.baidu.com#Intent;scheme=jsbridgedemo;package=com.fengdewang.hybrid_android;end
- Android出了scheme唤起以外,还可以使用intent协议唤起
知识扩展之 --- web ios android 之间的对比
- 下面表格 以前端工程的视角去理解
不会Java 也不会Objective-C 怎么办 /(ㄒoㄒ)/~~
- Java
- 相比于JavaScript,Java是强类型的语言,
- JavaScript声明的变量都是使用 var 去声明,而Java则必须用具体的类型去声明,例如声明 整数则用 int、声明字符串则用 String
- JavaScript中函数的返回值可以直接在函数最后 return出来,而Java中则需要在声明函数的时候声明返回值类型,例如 public String test(){},中的String则是函数的返回值类型
- Objective-C (OC)
- OC的语言设计是基于C语言的设计,也是强类型的语言,和Java一样声明变量需要规定类型。
- OC的方法声明和方法调用都是很反人类的
- + / - 分别代表此方法是类方法 和 实例方法
- 方法声明示例: methodName: (String *) str methodName2:(int) num
- 每多一个参数 都需要在方法名后面多加一个 空格和方法名
- 方法调用示例:[self methodName:str methodName2:int]
- OC中的self相当于js中this
- OC的方法调用不同于其他语言的 点方法调用(.),而是使用中括号空格 ([self methodName])
多学一门语言 扩展下自己的知识还是很有必要的,大家加油 (๑•̀ㅂ•́) ✧加油
如有错误,欢迎指正
编辑于 2018-01-15 14:41