stonerkuang 2020-06-10
有了它,Web 和 Native 可以进行交互,就像『进化药水』,让 Web 摇身一变,成为移动战场的『上将一名』。
实际上,JSBridge 其实真是一个很简单的东西,更多的是一种形式、一种思想。
任何一个移动操作系统中都包含可运行 JavaScript 的容器,例如 WebView 和 JSCore。
所以,运行 JavaScript 不用像运行其他语言时,要额外添加运行环境。因此,基于上面种种原因,JSBridge 应运而生。
PhoneGap(Codova 的前身)作为 Hybrid 鼻祖框架,应该是最先被开发者广泛认知的 JSBridge 的应用场景;
而对于 JSBridge 的应用在国内真正兴盛起来,则是因为杀手级应用微信的出现,主要用途是在网页中通过 JSBridge 设置分享内容。
移动端混合开发中的 JSBridge,主要被应用在两种形式的技术方案上:
基于 Web 的 Hybrid 解决方案:例如微信浏览器、各公司的 Hybrid 方案
非基于 Web UI 但业务逻辑基于 JavaScript 的解决方案:例如 React-Native
【注】:微信小程序基于 Web UI,但是为了追求运行效率,对 UI 展现逻辑和业务逻辑的 JavaScript 进行了隔离。因此小程序的技术方案介于上面描述的两种方式之间。
JSBridge 简单来讲,主要是 给 JavaScript 提供调用 Native 功能的接口,让混合开发中的『前端部分』可以方便地使用地址位置、摄像头甚至支付等 Native 功能。
实际上,JSBridge 就像其名称中的『Bridge』的意义一样,是 Native 和非 Native 之间的桥梁,它的核心是 构建 Native 和非 Native 间消息通信的通道,而且是 双向通信的通道。
JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等。
Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等。
这里有些同学有疑问了:消息都是单向的,那么调用 Native 功能时 Callback 怎么实现的? 对于这个问题,在下一节里会给出解释。
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 的实现原理。
Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通信原理会具有一些 Web 特性。
JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME。
注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
先解释一下 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,很容易丢失一些调用。
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单。
毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。
(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)
通信原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。这里,笔者推荐的实现方式如下:
JavaScript 调用 Native 推荐使用 注入 API 的方式。
Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可。
对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。
以 React Native 的 iOS 端举例:
JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是 为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。
从上面的剖析中,可以得知,JSBridge 的接口主要功能有两个:调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。因此,JSBridge 可以设计如下:
window.JSBridge = { // 调用 Native invoke: function(msg) { // 判断环境,获取不同的 nativeBridge nativeBridge.postMessage(msg); }, receiveMessage: function(msg) { // 处理 msg } };
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 中, 也会以此参数值作为句柄,调用相应的回调函数。
(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 一起发送给前端即可。
对于 JSBridge 的引用,常用有两种方式,各有利弊。
注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。
它的优点在于:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,它的缺点是:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。
直接与 JavaScript 一起执行。
与由 Native 端注入正好相反,它的优点在于:JavaScript 端可以确定 JSBridge 的存在,直接调用即可;缺点是:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。
这篇文章主要剖析的 JSBridge 的实现及应用,包括 JavaScript 与 Native 间的通信原理,JSBridge 的 JavaScript 端实现 以及 引用方式,并给出了一些示例代码,希望对读者有一定的帮助。