JSBridge的原理

关于 JSBridge,绝大多数同窗最先遇到的是微信的 WeiXinJSBridge(如今被封装成 JSSDK),各类 Web 页面能够经过 Bridge 调用微信提供的一些原生功能,为用户提供相关的功能。其实,JSBridge 很早就出如今软件开发中,在一些桌面软件中很早就运用了这样的形式,多用在通知、产品详情、广告等模块中,而后这些模块中,使用的是 Web UI,而相关按钮点击后,调用的是 Native 功能。如今移动端盛行,无论是 Hybrid 应用,仍是 React-Native 都离不开 JSBridge,固然也包括在国内举足轻重的微信小程序。那么,JSBridge 究竟是什么?它的出现是为了什么?它到底是怎么实现的?在这篇文章中,会在移动混合开发的范畴内,将给你们带来 JSBridge 的深刻剖析。 javascript

1 前言

有些童鞋听到 JSBridge 这个名词,就是以为很是高上大,有了它 Web 和 Native 能够进行交互,就像『进化药水』,让 Web 摇身一变,成为移动战场的『上将一名』。其实并不是如此,JSBridge 其实真是一个很简单的东西,更多的是一种形式、一种思想。前端

2 JSBridge 的起源

为何是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?java

固然不是由于 JavaScript 语言高人一等(虽然斯坦福大学已经把算法导论的语言从 Java 改为 JavaScript,小得意一下,嘻嘻),主要的缘由仍是由于 JavaScript 主要载体 Web 是当前世界上的 最易编写最易维护最易部署 的 UI 构建方式。工程师能够用很简单的 HTML 标签和 CSS 样式快速的构建出一个页面,而且在服务端部署后,用户不须要主动更新,就能看到最新的 UI 展示。web

所以,开发维护成本更新成本 较低的 Web 技术成为混合开发中几乎不二的选择,而做为 Web 技术逻辑核心的 JavaScript 也理所应当肩负起与其余技术『桥接』的职责,而且做为移动不可缺乏的一部分,任何一个移动操做系统中都包含可运行 JavaScript 的容器,例如 WebView 和 JSCore。因此,运行 JavaScript 不用像运行其余语言时,要额外添加运行环境。所以,基于上面种种缘由,JSBridge 应运而生。算法

PhoneGap(Codova 的前身)做为 Hybrid 鼻祖框架,应该是最早被开发者普遍认知的 JSBridge 的应用场景;而对于 JSBridge 的应用在国内真正兴盛起来,则是由于杀手级应用微信的出现,主要用途是在网页中经过 JSBridge 设置分享内容。小程序

移动端混合开发中的 JSBridge,主要被应用在两种形式的技术方案上:微信小程序

基于 Web 的 Hybrid 解决方案:例如微信浏览器、各公司的 Hybrid 方案浏览器

非基于 Web UI 但业务逻辑基于 JavaScript 的解决方案:例如 React-Native安全

【注】:微信小程序基于 Web UI,可是为了追求运行效率,对 UI 展示逻辑和业务逻辑的 JavaScript 进行了隔离。所以小程序的技术方案介于上面描述的两种方式之间。服务器

3 JSBridge 的用途

JSBridge 简单来说,主要是 给 JavaScript 提供调用 Native 功能的接口,让混合开发中的『前端部分』能够方便地使用地址位置、摄像头甚至支付等 Native 功能。

既然是『简单来说』,那么 JSBridge 的用途确定不仅『调用 Native 功能』这么简单宽泛。实际上,JSBridge 就像其名称中的『Bridge』的意义同样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通讯的通道,并且是 双向通讯的通道

所谓 双向通讯的通道:

JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。

Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。

这里有些同窗有疑问了:消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的? 对于这个问题,在下一节里会给出解释。

4 JSBridge 的实现原理

JavaScript 是运行在一个单独的 JS Context 中(例如,WebView 的 Webkit 引擎、JSCore)。因为这些 Context 与原生运行环境的自然隔离,咱们能够将这种状况与 RPC(Remote Procedure Call,远程过程调用)通讯进行类比,将 Native 与 JavaScript 的每次互相调用看作一次 RPC 调用。

在 JSBridge 的设计中,能够把前端看作 RPC 的客户端,把 Native 端看作 RPC 的服务器端,从而 JSBridge 要实现的主要逻辑就出现了:通讯调用(Native 与 JS 通讯)句柄解析调用。(若是你是个前端,并且并不熟悉 RPC 的话,你也能够把这个流程类比成 JSONP 的流程)

经过以上的分析,能够清楚地知晓 JSBridge 主要的功能和职责,接下来就以 Hybrid 方案 为案例从这几点来剖析 JSBridge 的实现原理。

4.1 JSBridge 的通讯原理

Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。所以,Hybrid 方案中 JSBridge 的通讯原理会具备一些 Web 特性。

4.1.1 JavaScript 调用 Native

JavaScript 调用 Native 的方式,主要有两种:注入 API拦截 URL SCHEME

4.1.1.1 注入API

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

对于 iOS 的 UIWebView,实例以下:

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

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

前端调用方式:

window.postBridgeMessage(message);
复制代码

对于 iOS 的 WKWebView 能够用如下方式:

@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    // 注入对象,前端调用其方法时,Native 能够捕获到
    [userCC addScriptMessageHandler:self name:@"nativeBridge"];

    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

    // TODO 显示 WebView
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeBridge"]) {
        NSLog(@"前端传递的数据 %@: ",message.body);
        // Native 逻辑
    }
}
复制代码

前端调用方式:

window.webkit.messageHandlers.nativeBridge.postMessage(message);
复制代码

对于 Android 能够采用下面的方式:

publicclassJavaScriptInterfaceDemoActivityextendsActivity{
private WebView Wv;

    @Override
    publicvoidonCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);

        Wv = (WebView)findViewById(R.id.webView);     
        final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);    	 

        Wv.getSettings().setJavaScriptEnabled(true);
        Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");

        // TODO 显示 WebView

    }

    publicclassJavaScriptInterface{
         Context mContext;

         JavaScriptInterface(Context c) {
             mContext = c;
         }

         publicvoidpostMessage(String webMessage){	    	
             // Native 逻辑
         }
     }
}
复制代码

前端调用方式:

1


window.nativeBridge.postMessage(message);
复制代码

在 4.2 以前,Android 注入 JavaScript 对象的接口是 addJavascriptInterface,可是这个接口有漏洞,能够被不法分子利用,危害用户的安全,所以在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口,解决安全问题。因此 Android 注入对对象的方式是 有兼容性问题的。(4.2 以前不少方案都采用拦截 prompt 的方式来实现,由于篇幅有限,这里就不展开了。)

4.1.1.2 拦截 URL SCHEME

先解释一下 URL SCHEME:URL SCHEME是一种相似于url的连接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 通常是自定义的,例如: qunarhy://hy/url?url=ymfe.tech,protocol 是 qunarhy,host 则是 hy。

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

在时间过程当中,这种方式有必定的 缺陷

使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。

建立请求,须要必定的耗时,比注入 API 的方式调用一样的功能,耗时会较长。

可是以前为何不少方案使用这种方式呢?由于它 支持 iOS6。而如今的大环境下,iOS6 占比很小,基本上能够忽略,因此并不推荐为了 iOS6 使用这种 并不优雅 的方式。

【注】:有些方案为了规避 url 长度隐患的缺陷,在 iOS 上采用了使用 Ajax 发送同域请求的方式,并将参数放到 head 或 body 里。这样,虽然规避了 url 长度的隐患,可是 WKWebView 并不支持这样的方式。

【注2】:为何选择 iframe.src 不选择 locaiton.href ?由于若是经过 location.href 连续调用 Native,很容易丢失一些调用。

4.1.2 Native 调用 JavaScript

相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟无论是 iOS 的 UIWebView 仍是 WKWebView,仍是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 便可。

Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,所以 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 本身都调用不了,更不用想让 Native 去调用了)

对于 iOS 的 UIWebView,示例以下:

result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
复制代码

对于 iOS 的 WKWebView,示例以下:

[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
复制代码

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

webView.loadUrl("javascript:" + javaScriptString);
复制代码

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

webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
    @Override
    publicvoidonReceiveValue(String value){

    }
});
复制代码

【注】:使用 loadUrl 的方式,并不能获取 JavaScript 执行后的结果。

4.1.3 通讯原理小总结

通讯原理是 JSBridge 实现的核心,实现方式能够各类各样,可是万变不离其宗。这里,笔者推荐的实现方式以下:

JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2如下使用 WebViewClient 的 onJsPrompt 方式)。

Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码便可。

对于其余方式,诸如 React Native、微信小程序 的通讯方式都与上描述的近似,并根据实际状况进行优化。

以 React Native 的 iOS 端举例:JavaScript 运行在 JSCore 中,实际上能够与上面的方式同样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并无设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 须要在 Object-C 去调 JavaScript 时才经过返回值触发调用。原理基本同样,只是实现方式不一样。

固然不只仅 iOS 和 Android,其余手机操做系统也用相应的 API,例如 WMP(Win 10)下能够用 window.external.notify 和 WebView.InvokeScript/InvokeScriptAsync 进行双向通讯。其余系统也相似。

4.2 JSBridge 接口实现

从上面的剖析中,能够得知,JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息)接被 Native 调用(接收 Native 消息)。所以,JSBridge 能够设计以下:

window.JSBridge = {
    // 调用 Native
    invoke: function(msg) {
        // 判断环境,获取不一样的 nativeBridge
        nativeBridge.postMessage(msg);
    },
    receiveMessage: function(msg) {
        // 处理 msg
    }
};
复制代码

在上面的文章中,提到过 RPC 中有一个很是重要的环节是 句柄解析调用 ,这点在 JSBridge 中体现为 句柄与功能对应关系。同时,咱们将句柄抽象为 桥名(BridgeName),最终演化为 一个 BridgeName 对应一个 Native 功能或者一类 Native 消息。 基于此点,JSBridge 的实现能够优化为以下:

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

JSBridge 大概的雏形出现了。如今终于能够着手解决这个问题了:消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的?

对于 JSBridge 的 Callback ,其实就是 RPC 框架的回调机制。固然也能够用更简单的 JSONP 机制解释:

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

因而可知,callback 参数这个 惟一标识 是这个回调逻辑的关键。这样,咱们能够参照这个逻辑来实现 JSBridge:用一个自增的惟一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 做为回溯的标识。这样,便可实现 Callback 回调逻辑。

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

    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 原封不动传回
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } elseif (bridgeName) {

            }
        }
    };
})();
复制代码

最后用一样的方式加上 Native 调用的回调逻辑,同时对代码进行一些优化,就大概实现了一个功能比较完整的 JSBridge。其代码以下:

(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); // 存储回调
        }
    };
})();
复制代码

固然,这段代码片断只是一个示例,主要用于剖析 JSBridge 的原理和流程,里面存在诸多省略和不完善的代码逻辑,读者们能够自行完善。

【注】:这一节主要讲的是,JavaScript 端的 JSBridge 的实现,对于 Native 端涉及的并很少。在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单,主要的代码逻辑是:接收到 JavaScript 消息 => 解析参数,拿到 bridgeName、data 和 callbackId => 根据 bridgeName 找到功能方法,以 data 为参数执行 => 执行返回值和 callbackId 一块儿回传前端。 Native 调用 JavaScript 也一样简单,直接自动生成一个惟一的 ResponseId,并存储句柄,而后和 data 一块儿发送给前端便可。

5 JSBridge 如何引用

对于 JSBridge 的引用,经常使用有两种方式,各有利弊。

5.1 由 Native 端进行注入

注入方式和 Native 调用 JavaScript 相似,直接执行桥的所有代码。

它的优势在于:桥的版本很容易与 Native 保持一致,Native 端不用对不一样版本的 JSBridge 进行兼容;与此同时,它的缺点是:注入时机不肯定,须要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,须要优先判断 JSBridge 是否已经注入成功。

5.2 由 JavaScript 端引用

直接与 JavaScript 一块儿执行。

与由 Native 端注入正好相反,它的优势在于:JavaScript 端能够肯定 JSBridge 的存在,直接调用便可;缺点是:若是桥的实现方式有更改,JSBridge 须要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。

6 总结

这篇文章主要剖析的 JSBridge 的实现及应用,包括 JavaScript 与 Native 间的通讯原理JSBridge 的 JavaScript 端实现 以及 引用方式,并给出了一些示例代码,但愿对读者有必定的帮助。

相关文章
相关标签/搜索