使用 WebSocket 實現 JsBridge
去年寫了個簡單的 Android 殼子程式在部門內部使用,藉助殼子程式 JavaScript (以下簡稱 JS)可以高效地使用拍照、簽名、二維碼掃描等原生功能,為 Web 專案提供接近原生的體驗。但是一段時間使用下來,前端開發人員陸陸續續地反饋了一些蛋疼的問題,比如正常情況下可以使用 Chrome 瀏覽器的chrome://inspect
功能除錯裝置上的遠端網頁,但是一些裝置死活都無法inspect
,這給開發除錯帶來了很多不便,而後我在思考,還有沒有另一種方式既能實現基本的 JsBridge 功能又能方便開發人員除錯。
我想到了 Socket/">WebSocket,它是 HTML5 開始提供的一種在單個 TCP 連線上進行全雙工通訊的協議。不過,在具體介紹 Websocket 之前先簡單回顧下目前主流 JsBridge 的實現方案。
1. addJavascriptInterface 方式
這是 Android 官方推薦的互動方式,但是在 Android 4.2 以下存在安全漏洞,後期官方通過在 Java 遠端方法上添加註解@JavascriptInterface
解決了這一安全隱患。
1.1 Java 呼叫 JS 方法
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 端注入介面等待呼叫:
class BridgeInterface { @JavascriptInterface public String call(String msg) { return "Hello!"; } } ... webView.addJavascriptInterface(new BridgeInterface(), "JsBridge");
JS 端呼叫:
if(window.JsBridge){ JsBridge.call(msg); }
2. onJsPrompt 方式
為解決 Android 4.2 以下安全漏洞,肯定不能再使用addJavascriptInterface
方式了,我們可以使用 HTML DOMprompt()
方法配合WebChromeClient
的onJsPrompt
回撥方法實現互動。
Java 端等待回撥:
@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 端呼叫:
prompt(message, defaultValue);
3. 攔截 Url 方式
ofollow,noindex">https://github.com/lzyzsd/JsBridge
提供了一個很好的思路,通過shouldOverrideUrlLoading
來攔截指定規則的 Url,然後處理業務邏輯實現兩端互動。
由於 Java 呼叫 JS 方法都大同小異,這裡我們不做過多的說明。
頁面載入完成後,JsBridge 動態建立了一個不可見的iframe
,在需要通知 Java 端時在 JS訊息佇列裡新增新的訊息,同時改變iframe
的src
值觸發 WebView 的shouldOverrideUrlLoading
回撥方法,然後 Java 端呼叫javascript:WebViewJavascriptBridge._fetchQueue();
方法準備獲取資料,呼叫後觸發iframe
的src
值改變,此時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 搭建伺服器,示例程式碼如下:
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 客戶端
// 建立完物件後,客戶端就會與伺服器進行連線 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 。