hybird知识总结

正式接手第一个hybrid项目,智能客服系统,系统学习总结一下

Posted by Liangyuqi on July 5, 2018
view times

一、一些基础

定义:Hybrid是半Native半web开发模式 Hybrid模式中,底层功能API均由原生容器通过某种方式提供,然后业务逻辑由H5页面完成,最终原生容器加载H5页面,完成整个App

实现过程:前端做好静态页面打包成文件交给客户端,客户端拿到前端静态页面,以文件形式存储在app中。客户端在一个webview中,使用file协议加载静态页面。

版本更新:客户端每次启动,都去server端检查版本号。如果server端版本号大于客户端版本号,就去下载最新的zip包,下载完之后解压包,然后将现有文件覆盖。

webview:是app中的一个组件,app可以有webview,也可以没有。用于加载H5页面等,即一个小型的浏览器内核,这里是一个统称。iOS 的是 UIWebView 或 WKWebView,Android 的是WebView。

二、传统JavaScript调用Native

注入 API

注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到JavaScript 调用 Native 的目的

对于 iOS 的 UIWebView,实例如下:

#import <JavaScriptCore/JavaScriptCore.h> //引入官方的库文件

JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) {
    // Native 逻辑
};

前端调用方式:

window.postBridgeMessage(message);

ios7才出现官方提供的第三方库”JavaScriptCore”,即可开放api给JS调用。

拦截 URL scheme

URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的。

例如:weixin://dl/scan 扫一扫

拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。

demo: https://github.com/liangyuqi/hybrid-weixin(只是演示,无法正常运行,微信有严格的权限验证,外部页面不能随意使用schema)

优点:支持ios6

缺点:1.url长度限制,2.比起注入更为耗时

三、传统Native调用Javascript

Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。

对于 iOS 的 UIWebView,示例如下:

result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];

对于 iOS 的 WKWebView,示例如下:

[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];

对于 Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码,来实现:

webView.loadUrl("javascript: 方法名('参数,需要转为字符串')");

而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现:

webView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
    @Override
    public void onReceiveValue(String value){
		//这里的value即为对应JS方法的返回值
    }
});
  • 4.4之前Native通过loadUrl来调用JS方法,只能让某个JS方法执行,但是无法获取该方法的返回值
  • 4.4之后,通过evaluateJavascript异步调用JS方法,并且能在onReceiveValue中拿到返回值
  • 不适合传输大量数据(大量数据建议用接口方式获取)
  • webView.loadUrl(“javascript: 方法名(‘参数,需要转为字符串’)”);函数需在UI线程运行,因为webView为UI控件(但是有一个坏处是会阻塞UI线程)

四、关于JSBridge

定义:JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用Native。

核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道

这里实现一下注入api的方法,可以理解为JSBridge是一种交互理念,之前提过的url scheme也是其中的一种实现,JSBridge交互的一个很大特点就是便于拓展,而且没有重大的安全性问题,所以也就是为什么它广为流行。

JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)

一个 BridgeName(桥名)对应一个 Native 功能或者一类 Native 消息。

window.JSBridge = {
    // 调用 Native
    invoke: function(bridgeName, data) {
        // 判断环境,获取不同的 nativeBridge
        nativeBridge.postMessage({
            bridgeName: bridgeName,
            data: data || {}
        });
    },
    receiveMessage: function(msg) {
        var bridgeName = msg.bridgeName,
            data = msg.data || {};
        // 具体逻辑
    }
};

简单雏形有了,但是消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?

类似jsonp请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数。

用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

(function () {
    var id = 0,
        callbacks = {},
        registerFuncs = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();