Hybrid开发中JSBridge的实现

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兼容代码
  • 示例:github.com/q946401639/H
 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的调用需要注意哪些事项

  • 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

知识扩展之 --- 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