使用 WebSocket 实现 JsBridge

去年写了个简单的 Android 壳子程序在部门内部使用,借助壳子程序 JavaScript (以下简称 JS)可以高效地使用拍照、签名、二维码扫描等原生功能,为 Web 项目提供接近原生的体验。但是一段时间使用下来,前端开发人员陆陆续续地反馈了一些蛋疼的问题,比如正常情况下可以使用 Chrome 浏览器的 chrome://inspect 功能调试设备上的远程网页,但是一些设备死活都无法 inspect,这给开发调试带来了很多不便,而后我在思考,还有没有另一种方式既能实现基本的 JsBridge 功能又能方便开发人员调试。

我想到了 WebSocket,它是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。不过,在具体介绍 Websocket 之前先简单回顾下目前主流 JsBridge 的实现方案。

1. addJavascriptInterface 方式

这是 Android 官方推荐的交互方式,但是在 Android 4.2 以下存在安全漏洞,后期官方通过在 Java 远程方法上添加注解 @JavascriptInterface 解决了这一安全隐患。

1.1 Java 调用 JS 方法

1
2
3
4
5
6
7
8
9
10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(script, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
// 处理 JavaScript 方法的返回值
}
});
} else {
webView.loadUrl("javascript:" + script);// 低版本通用方式,但有弊端:无法获取返回值
}

1.2 JS 调用 Java 方法

Java 端注入接口等待调用:

1
2
3
4
5
6
7
8
class BridgeInterface {    
@JavascriptInterface
public String call(String msg) {
return "Hello!";
}
}
...
webView.addJavascriptInterface(new BridgeInterface(), "JsBridge");

JS 端调用:

1
2
3
if(window.JsBridge){
JsBridge.call(msg);
}

2. onJsPrompt 方式

为解决 Android 4.2 以下安全漏洞,肯定不能再使用 addJavascriptInterface 方式了,我们可以使用 HTML DOM prompt() 方法配合 WebChromeClientonJsPrompt 回调方法实现交互。

Java 端等待回调:

1
2
3
4
5
@Override
public boolean onJsPrompt(WebView view, String url, final String message, String defaultValue, final JsPromptResult result) {
// message 和 defaultValue 为 JavaScript 端传来参数
// 我们还可以通过 JsPromptResult 返回数据给 JavaScript 端,例如:result.confirm("Done!");
}

JS 端调用:

1
prompt(message, defaultValue);

3. 拦截 Url 方式

https://github.com/lzyzsd/JsBridge 提供了一个很好的思路,通过 shouldOverrideUrlLoading 来拦截指定规则的 Url,然后处理业务逻辑实现两端交互。
由于 Java 调用 JS 方法都大同小异,这里我们不做过多的说明。

页面加载完成后,JsBridge 动态创建了一个不可见的 iframe,在需要通知 Java 端时在 JS 消息队列里添加新的消息,同时改变 iframesrc 值触发 WebView 的 shouldOverrideUrlLoading 回调方法,然后 Java 端调用 javascript:WebViewJavascriptBridge._fetchQueue(); 方法准备获取数据,调用后触发 iframesrc 值改变,此时 src 值为需要传递给 Java 端的数据,最后再次触发 shouldOverrideUrlLoading 回调方法,Java 端解析 url 取出数据,完成整个调用流程。

整个流程稍显啰嗦了一点,JS 端发消息给 Java 端完全可以把两步合并为一步,此外通过改变 iframe src 属性的这种方式并不能保证 shouldOverrideUrlLoading 每次都会被调用。

4. WebSocket 方式

在使用之前,我对 WebSocket 并不熟悉,网上找不到使用它来实现 JsBridge 的相关资料,不过这并不能阻止我尝试的脚步。

WebSocket 协议在 2008年 诞生,2011 年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其特点包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。

WebSocket 的相关特性刚好满足 Native 端和 JS 端交互。

4.1 搭建 Android 端 WebSocket 服务器

这里用的是开源项目 AndroidAsync 搭建服务器,示例代码如下:

1
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
AsyncHttpServer server = new AsyncHttpServer();

List<WebSocket> _sockets = new ArrayList<WebSocket>();

server.websocket("/live", new WebSocketRequestCallback() {
@Override
public void onConnected(final WebSocket webSocket, AsyncHttpServerRequest request) {
_sockets.add(webSocket);

//连接断开时的回调
webSocket.setClosedCallback(new CompletedCallback() {
@Override
public void onCompleted(Exception ex) {
try {
if (ex != null)
Log.e("WebSocket", "Error");
} finally {
_sockets.remove(webSocket);
}
}
});

// 我们可以在这里处理来自 JS 端的消息
webSocket.setStringCallback(new StringCallback() {
@Override
public void onStringAvailable(String s) {
if ("Hello Server".equals(s))
webSocket.send("Welcome Client!");// Java 端发消息给 JS 端
}
});

}
});

server.listen(5000);

4.2 JS 实现 WebSocket 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建完对象后,客户端就会与服务器进行连接
var ws = new WebSocket("wss://localhost:5000/live");

ws.onopen = function(evt) {
// 连接成功后
console.log("Connection open ...");
};

ws.onmessage = function(evt) {
// 收到服务端的消息
console.log( "Received Message: " + evt.data);
};

ws.onclose = function(evt) {
// 连接断开后
console.log("Connection closed.");
};

...

// JS 端发消息给服务端
ws.send("Hello Server");

写了个测试 Demo,发现整体效果还是可以的,后期完善下可以独立出来做个开源组件了。

总结

WebSocket 并不是新鲜技术,用它来实现 JsBridge 也有点杀鸡用牛刀的错觉,但也不失为一种新的思路。此外,通过这种方式我们可以不使用 chrome://inspect 来调试远程网页了,把 JS 里 localhost 改为手机的 IP 地址就可以实现在任意电脑上调试 JsBridge了。

注意,原生 WebView 在 Android 4.4.x (KitKat) 之后才支持 WebSocket,如果想兼容低版本可以尝试使用一些第三方 WebView 组件,例如腾讯浏览服务 TBS

参考

  1. Android WebView 的 Js 对象注入漏洞解决方案
  2. JsBridge 使用和原理
  3. JsBridge
  4. WebViewJavascriptBridge
  5. WebSocket 教程