jsBridge的原理

作者: 幻城之雪 | 来源:发表于2023-04-26 21:54 被阅读0次
image

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

1.起源

为什么是 JSBridge ?而不是 PythonBridge 或是 RubyBridge ?

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

因此,开发维护成本 和 更新成本 较低的 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 进行了隔离。因此小程序的技术方案介于上面描述的两种方式之间。

2.用途

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

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

所谓双向通信的通道:

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

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

3.实现原理

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 的实现原理。

3.1 JSBridge 的通信原理

Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通信原理会具有一些 Web 特性。

3.1.1 JavaScript调用Native

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

3.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 可以采用下面的方式:

public class JavaScriptInterface DemoActivityextendsActivity{

private WebView Wv;

@Override

public void onCreate(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

}

public class JavaScriptInterface{

Context mContext;

JavaScriptInterface(Context c) {

  mContext = c;

}

public void postMessage(String webMessage){

  // Native 逻辑

}

}

}

前端调用方式:

window.nativeBridge.postMessage(message);

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

3.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(包括所带的参数)进行相关操作。

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

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

2)创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。

但是之前为什么很多方案使用这种方式呢?因为它 支持 iOS6。而现在的大环境下,iOS6 占比很小,基本上可以忽略,所以并不推荐为了 iOS6 使用这种 并不优雅 的方式。

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

【注2】:为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过location.href 连续调用 Native,很容易丢失一些调用。

3.1.2Native 调用 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

    public void onReceiveValue(String value){    }

});

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

3.1.3 通信原理小总结

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

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

2)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 进行双向通信。其他系统也类似。

3.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 的 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); // 执行调用

                  }

            } else if (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); // 存储回调

}};

})();

4 JSBridge 如何引用

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

4.1 由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。

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

4.2 由 JavaScript 端引用

直接与 JavaScript 一起执行。

与由 Native 端注入正好相反,它的优点在于:JavaScript 端可以确定 JSBridge 的存在,直接调用即可;缺点是:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。

5 总结

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

相关文章

网友评论

    本文标题:jsBridge的原理

    本文链接:https://www.haomeiwen.com/subject/byfbjdtx.html