hybrid app 梳理

1,447 阅读9分钟

什么是 Hybird app

hybrid 这个单词在英文中表示 “混合的、杂交的”;顾名思义,hybrid app的意思就是混合模式应用程序。也就是指介于 Web App、Native App 这两者之间的 app。

通俗的来说,hybird app就是 Native App通过 Webview组件(一个浏览器内核)将 H5页面内嵌到应用内后,形成的一种克服原生应用修改后必须重新发版的天然不足的混合模式应用程序。

Hybird 的分类

  • 多View混合型

    这种模式主要特点是将webview作为Native中的一个view组件,当需要的时候在独立运行显示,也就是说主体是Native,web技术只是起来一些补充作用

    这种模式几乎就是原生开发,没有降低什么难度,到了16年几乎已经没人使用了

  • 单View混合型

    这种模式是在同一个view内,同时包括Native view和webview(互相之间是层叠的关系),比如一些应用会用H5来加载百度地图作为整个页面的主体内容,然后再webview之上覆盖一些原生的view,比如搜索什么的

    这种模式开发完成后体验较好,但是开发成本较大,一般适合一些原生人员使用

  • Web主体型

    这种模式算是传统意义上的Hybrid开发,很多Hybrid框架都是基于这种模式的,比如PhoneGap,AppCan,Html5+等

    这种模式的一个最大特点是,Hybrid框架已经提供各种api,打包工具,调试工具,然后实际开发时不会使用到任何原生技术,实际上只会使用H5和js来编写,然后js可以调用原生提供的api来实现一些拓展功能。往往程序从入口页面,到每一个功能都是h5和js完成的

    理论上来说,这种模式应该是最佳的一种模式(因为用H5和js编写最为快速,能够调用原生api,功能够完善),但是由于一些webview自身的限制,导致了这种模式在性能上损耗不小,包括在一些内存控制上的不足,所以导致体验要逊色于原生不少

    当然了,如果能解决体验差问题,这种模式应当是最优的(比如由于iOS对H5支持很好,iOS上的体验就很不错)

  • 多主体共存型(灵活型)

    这种模式的存在是为了解决web主体型的不足,这种模式的一个最大特点是,原生开发和h5开发共存,也就是说,对于一些性能要求很高的页面模块,用原生来完成,对于一些通用型模块,用h5和js来完成

    这种模式通用有跨平台特性,而且用户体验号,性能高,不逊色与原生,但是有一个很大的限制就是,采用这种模式需要一定的技术前提

    也就是说这种模式不同于web主体型可以直接用第三方框架,这种模式一般是一些有技术支持的公司自己实现的,包括H5和原生的通信,原生API提供,容器的一些处理全部由原生人员来完成,所以说,使用这种技术的前提是得有专业的原生人员(包括Android,iOS)以及业务开发人员(原生开发负责功能,前端解决简单通用h5功能)

    当然了,如果技术上没有问题,用这种方案开发出来的App体验是很好的,而且性能也不逊色原生,所以是一种很优的方案

Native与h5交互原理

既然是混合模式,Native如何与h5进行交互就是一个核心的问题。其交互方式有以下两种

  • Android、iOS原生和H5的基本通讯机制
  • 基于Jsbridge的通讯机制

Android、IOS原生和H5的基本通讯机制

由于 h5 是依附于 Android、iOS 的 Webview 组件上的,所以 Android、iOS 可以通过 Webview 直接调用 JavaScript 代码。

Android原生和H5的基本通讯机制
// 4.4版本以前,该方法没有系统版本的限制,但是无法获取函数的返回值。需要使用 prompt 方法进行兼容,让 H5端 通过 prompt 进行数据的发送,客户端进行拦截并获取数据。
mWebView = new WebView(this);		
mWebView.loadUrl("javascript: 方法名('参数,须要转为字符串')"); 
runOnUiThread(new Runnable() {  
        @Override  
        public void run() {
            //函数需在UI线程运行,因为mWebView为UI控件,会阻塞UI线程。
            mWebView.loadUrl("javascript: 方法名('参数,须要转为字符串')");  
            Toast.makeText(Activity名.this, "调用方法...", Toast.LENGTH_SHORT).show();  
        }  
}); 

//4.4版本以后,异步执行JS代码,并获取返回值	
mWebView.evaluateJavascript("javascript: 方法名('参数,须要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
    		//这里的value即为对应JS方法的返回值
        }
});	
IOS原生和H5的基本通讯机制

stringByEvaluatingJavaScriptFromString可以取得JS函数执行的返回值,但是方法必须是绑定在顶层页面的window上对象,如:window.top.foo

// Swift 
webview.stringByEvaluatingJavaScriptFromString("方法名(参数)")
// OC 
[webView stringByEvaluatingJavaScriptFromString:@"方法名(参数);"];
JavaScript 调用 IOS

引入官方的库文件 #import <JavaScriptCore/JavaScriptCore.h>, Native 注册 api

//webview加载完毕后设置一些js接口
-(void)webViewDidFinishLoad:(UIWebView *)webView{
    [self hideProgress];
    [self setJSInterface];
}

-(void)setJSInterface{
    
    JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    
    // 注册名为foo的api方法
    context[@"foo"] = ^() {
    	
    	//获取参数
        NSArray *args = [JSContext currentArguments];
        NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
        //作一些本身的逻辑
        //返回一个值  'foo:'+title
        return [NSString stringWithFormat:@"foo:%@", title];
    };
}	

JavaScript调用原生的代码

//调用方法,用top是确保调用到最顶级,由于iframe要用top才能拿到顶级
window.top.foo('test'); //返回:'foo:test'

Native 中经过引入官方提供的 JavaScriptCore 库( iOS7 中出现的),将 api 绑定到 JSContext 上,由此Javascript 可以调用 iOS 所注册的 api(通过window.top.*)。

基于Jsbridge的通讯机制

首先,web 发起网络请求主要通过以下几个途径

  • location.href
  • a 标签
  • ajax
  • iframe

而 Native 并不能拦截由 ajax 所发出的请求;location.href 连续多次进行跳转时,webview会直接过滤掉后发的跳转请求;所以在 h5 中,如果需要通过跳转链接,并且需要客户端准确无误的感知则一般会使用 iframe 进行。

基于以上,在 Jsbridge 中, h5 与 Native 的交互原理主要是依靠拦截页面动作,来做出相应的动作。Native 可以从以下两点进行拦截:

  1. WebView URL Scheme 跳转拦截
  2. WebView 中的prompt/console/alert拦截:通常使用prompt,因为这个方法在前端中使用频率低,比较不会出现冲突

下面主要阐述下 Webview URL Scheme 拦截。

什么是 URL Scheme

URL Scheme 是一种类似于url的链接(可拼入参数), 是为了方便 App 之间互相调用设计的。可以用系统的 OpenURI 打开,然后系统会进行判断,如果是系统的 url scheme,则打开系统应用,否则找看是否有 App 注册这种 scheme,打开对应App。

网上一些 scheme 整理:

h5 基于 scheme 与 Native 交互原理

image.png

通过上图可以看出,h5 与 Native 的交互流程主要是通过触发一个 scheme 来完成。而完成触发这个动作需要一个中间件可以连接 h5 与 Native,这个就是 JsBridge。通过 JsBridge,需要思考下列几个问题:

  1. JsBridge 需要具备什么功能
  2. Javascript 如何基于 JsBridge 与 Native 进行交互
  3. Native 如何基于 JsBridge 与 h5 进行交互

查看下图

image.png

JsBridge 从哪里来,具备什么功能

JsBridge 是一个挂载在 window 上的一个对象,由 Native 注入;它里面的字段都是约定的,因为在 Native 执行初始化 JS 的时候会用到,比如 WVJBCallbacks。初始化代码如下

;(function() {
    //如果已经初始化了,则返回。
    if (window.WebViewJavascriptBridge) {
        return;
    }
    if (!window.onerror) {
        window.onerror = function(msg, url, line) {
            console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
        }
    }
    //初始化一些属性。
    var messagingIframe;
    //用于存储消息列表
    var sendMessageQueue = [];
    //用于存储消息
    var messageHandlers = {};
    //通过下面两个协议组合来确定是否是特定的消息,然后拦击。
    var CUSTOM_PROTOCOL_SCHEME = 'https';
    var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
    //oc调用js的回调
    var responseCallbacks = {};
    //消息对应的id
    var uniqueId = 1;
    //是否设置消息超时
    var dispatchMessagesWithTimeoutSafety = true;
    //web端注册一个消息方法
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    //web端调用一个OC注册的消息
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ handlerName: handlerName, data: data }, responseCallback);
    }
    function disableJavscriptAlertBoxSafetyTimeout() {
        dispatchMessagesWithTimeoutSafety = false;
    }
        //把消息转换成JSON字符串返回
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    //OC调用JS的入口方法
    function _handleMessageFromObjC(messageJSON) {
        _dispatchMessageFromObjC(messageJSON);
    }

    //初始化桥接对象,OC可以通过WebViewJavascriptBridge来调用JS里面的各种方法。
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };


    //处理从OC返回的消息。
    function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
            _doDispatchMessageFromObjC();
        }

        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;
            //回调
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {//主动调用
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData });
                    };
                }
                //获取JS注册的函数
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    //调用JS中的对应函数处理
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    //把消息从JS发送到OC,执行具体的发送操作。
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            //存储消息的回调ID
            responseCallbacks[callbackId] = responseCallback;
            //把消息对应的回调ID和消息一起发送,以供消息返回以后使用。
            message['callbackId'] = callbackId;
        }
        //把消息放入消息列表
        sendMessageQueue.push(message);
        //下面这句话会出发JS对OC的调用
        //让webview执行跳转操作,从而可以在
        //webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 中拦截到JS发给OC的消息
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }


    messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    //messagingIframe.body.style.backgroundColor="#0000ff";
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    document.documentElement.appendChild(messagingIframe);


    //注册_disableJavascriptAlertBoxSafetyTimeout方法,让OC可以关闭回调超时,默认是开启的。
    registerHandler("_disableJavascriptAlertBoxSafetyTimeout", disableJavscriptAlertBoxSafetyTimeout);
    //执行_callWVJBCallbacks方法
    setTimeout(_callWVJBCallbacks, 0);

    //初始化WEB中注册的方法。这个方法会把WEB中的hander注册到bridge中。
    //下面的代码其实就是执行WEB中的callback函数。
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i = 0; i < callbacks.length; i++) {
            callbacks[i](WebViewJavascriptBridge);
        }
    }
})();

JsBridge 担任了中间人的角色。因此,它需要向 h5 提供注册回调函数的方法

registerHandler(cmd, callback)

以及调用 Native 方法的接口

callHandler(cmd, ...params, callback)

其中 cmd 是 Native 开发人员与web开发人员统一约定的标识符,可以想象成函数名称。JsBridge 对象拥有的方法和属性如下图

image.png

Javascript 如何基于 JsBridge 与 Native 进行交互

h5 通过调用 JsBridge.callHandler 与 Native 进行交互。在调用该方法后, JsBridge 内容进行了如下步骤:

  1. 判断是否有回调函数,如果有,生成一个回调函数id,并将id和对应回调添加进入回调函数集合responseCallbacks

  2. 通过特定的参数转换方法,将传入的数据,方法名一起,拼接成一个 url scheme (格式如:CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data)

  3. 使用内部早就创建好的一个隐藏iframe来触发scheme

    //创建隐藏iframe过程
    var messagingIframe = document.createElement('iframe');
    messagingIframe.style.display = 'none';
    document.documentElement.appendChild(messagingIframe);
    
    //触发scheme
    messagingIframe.src = uri;
    					
    
Native 如何基于 JsBridge 与 h5 进行交互

Native 在捕捉到 url scheme 后,根据其格式解析该 url,获取所需回调函数id、参数等信息。进行下面步骤:

  1. 根据 api 名,在本地找寻对应的 api 方法,并且记录该方法执行完后的回调函数 id
  2. 根据提取出来的参数,根据定义好的参数进行转化 (如果是JSON格式需要手动转换,如果是String格式直接可以使用)
  3. 原生本地执行对应的api功能方法
  4. 功能执行完毕后,找到这次 api 调用对应的回调函数 id,然后连同需要传递的参数信息,组装成一个 JSON 格式的参数,回调的 JSON 格式为: {responseId:回调id,responseData:回调数据}
    • responseId<String>:  H5页面中对应需要执行的回调函数的id,在H5中生成url scheme时就已经产生
    • responseData<JSON>: Native需要传递给H5的回调数据

参考