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


# 前置内容
# jsbridge 的起源
为什么是 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 进行了隔离。因此小程序的技术方案介于上面描述的两种方式之间
# js 中的 eval 函数
try {
var fn = eval('WebViewJavascriptBridge.' + handlerName);
} catch (e) {
console.log(e);
}
2
3
4
5
这段代码是 JavaScript 中的一个片段, 它尝试通过字符串拼接来构造一个函数名, 然后使用 eval 函数来执行这个函数。具体来说:
handlerName是一个变量, 它包含一个函数名的字符串, 例如 "myFunction"eval函数用于将字符串解析为 JavaScript 代码并执行。在这里, 它将字符串 "WebViewJavascriptBridge.myFunction" 解析为一个函数并执行它try...catch语句用于捕获可能在 eval 执行时发生的异常。如果构造的函数名无效或不存在, eval 会引发一个异常, 该异常将被 catch 块捕获, 并在控制台上记录异常信息
# 什么是双向通道
JSBridge 就像其名称中的『Bridge』的意义一样, 是 Native 和非 Native 之间的桥梁, 它的核心是 构建 Native 和非 Native 间消息通信的通道, 而且是 双向通信的通道

所谓 双向通信的通道:
- JS 向 Native 发送消息 : 调用相关功能、通知 Native 当前 JS 的相关状态等
- Native 向 JS 发送消息 : 回溯调用结果、消息推送、通知 JS 当前 Native 的状态等
# 实现原理
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 的实现原理
# js 通信原理
# JavaScript 调用 Native
Android
一、addJavascriptInterface
在 4.2 版本之前 Android 注入 JavaScript 对象的接口是 addJavascriptInterface。 在 4.2 中引入新的接口 @JavascriptInterface(上面代码中使用的)来替代这个接口, 解决安全问题。所以 Android 注入对对象的方式是 有兼容性问题的
二、拦截 URL SCHEME
Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求, 之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作
在时间过程中, 这种方式有一定的 缺陷:
- 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患
- 创建请求, 需要一定的耗时, 比注入 API 的方式调用同样的功能, 耗时会较长
但是之前为什么很多方案使用这种方式呢?因为它 支持 iOS6
【注】: 有些方案为了规避 url 长度隐患的缺陷, 在 iOS 上采用了使用 Ajax 发送同域请求的方式, 并将参数放到 head 或 body 里。这样, 虽然规避了 url 长度的隐患, 但是 WKWebView 并不支持这样的方式
【注 2】: 为什么选择 iframe.src 不选择 locaiton.href ?因为如果通过 location.href 连续调用 Native, 很容易丢失一些调用
# Native 调用 JavaScript
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单, 毕竟不管是 iOS 的 UIWebView 还是 WKWebView, 还是 Android 的 WebView 组件, 都以子组件的形式存在于 View/Activity 中, 直接调用相应的 API 即可
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){
}
});
2
3
4
5
6
【注】: 使用 loadUrl 的方式, 并不能获取 JavaScript 执行后的结果
iOS
# JSBridge 接口实现
从上面的剖析中, 可以得知, JSBridge 的接口主要功能有两个: 调用 Native(给 Native 发消息) 和 接被 Native 调用(接收 Native 消息)。因此, JSBridge 可以设计如下:
window.JSBridge = {
// 调用 Native
invoke: function (msg) {
// 判断环境, 获取不同的 nativeBridge
nativeBridge.postMessage(msg);
},
receiveMessage: function (msg) {
// 处理 msg
},
};
2
3
4
5
6
7
8
9
10
在上面的文章中, 提到过 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 || {};
// 具体逻辑
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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) {
}
}
};
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
最后用同样的方式加上 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); // 存储回调
}
};
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
当然, 这段代码片段只是一个示例, 主要用于剖析 JSBridge 的原理和流程, 里面存在诸多省略和不完善的代码逻辑, 读者们可以自行完善
在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单, 主要的代码逻辑是
- 接收到 JavaScript 消息
- 解析参数, 拿到 bridgeName、data 和 callbackId
- 根据 bridgeName 找到功能方法, 以 data 为参数执行
- 执行返回值和 callbackId 一起回传前端
Native 调用 JavaScript 也同样简单, 直接自动生成一个唯一的 ResponseId, 并存储句柄, 然后和 data 一起发送给前端即可
# TODO
- jsbridge 源码中为什么有两个 iframe, 是为了解决什么问题