如何使用 WebMessagePort 作为 addJavascriptInterface() 的替代品?

How Do You Use WebMessagePort As An Alternative to addJavascriptInterface()?

Google 对 Android 应用程序开发人员的 security guidelines 具有以下内容:

WebViews do not use addJavaScriptInterface() with untrusted content.

On Android M and above, HTML message channels can be used instead.

据我所知,“HTML 消息渠道”指的是 createWebMessageChannel(), WebMessagePort, WebMessage 和 kin.

但是,他们没有提供任何示例。他们所做的只是 link 到 WhatWG specification, which is rather unclear. And, based on a Google search for createWebMessageChannel, it appears that this has not been used much yet — my blog post describing changes in the Android 6.0 SDK 进入前 10 名搜索结果,我只是顺便提一下。

addJavascriptInterface() 用于允许 WebView 中的 Java 脚本调用应用程序使用 WebView 提供的 Java 代码。我们如何使用“HTML 消息通道”来替代它?

CTS 中有针对它的测试

// Create a message channel and make sure it can be used for data transfer to/from js.
public void testMessageChannel() throws Throwable {
    if (!NullWebViewUtils.isWebViewAvailable()) {
        return;
    }
    loadPage(CHANNEL_MESSAGE);
    final WebMessagePort[] channel = mOnUiThread.createWebMessageChannel();
    WebMessage message = new WebMessage(WEBVIEW_MESSAGE, new WebMessagePort[]{channel[1]});
    mOnUiThread.postWebMessage(message, Uri.parse(BASE_URI));
    final int messageCount = 3;
    final CountDownLatch latch = new CountDownLatch(messageCount);
    runTestOnUiThread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < messageCount; i++) {
                channel[0].postMessage(new WebMessage(WEBVIEW_MESSAGE + i));
            }
            channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
                @Override
                public void onMessage(WebMessagePort port, WebMessage message) {
                    int i = messageCount - (int)latch.getCount();
                    assertEquals(WEBVIEW_MESSAGE + i + i, message.getData());
                    latch.countDown();
                }
            });
        }
    });
    // Wait for all the responses to arrive.
    boolean ignore = latch.await(TIMEOUT, java.util.concurrent.TimeUnit.MILLISECONDS);
}

文件:cts/tests/tests/webkit/src/android/webkit/cts/PostMessageTest.java。 至少有一些起点。

好的,我有这个工作,虽然有点糟。

第 1 步:使用 loadDataWithBaseURL() 填充您的 WebViewloadUrl() 将不起作用,因为 bugs。您需要使用 httphttps URL 作为 loadDataWithBaseURL() 的第一个参数(或者至少不是 file,因为存在错误)。稍后您将需要 URL,因此请保留它(例如,private static final String 值)。

步骤 #2:决定何时要初始化从 Java脚本到 Java 的通信。使用 addJavascriptInterface(),可以立即使用。但是,使用 WebMessagePort 并不是那么好。特别是,您不能在页面加载之前尝试初始化通信(例如,WebViewClient 上的 onPageFinished())。

第 3 步:在您想要初始化这些通信时,在 WebView 上调用 createWebMessageChannel() 以创建 WebMessagePort[]。该数组中的第 0 个元素是通信管道的末端,您可以对其调用 setWebMessageCallback() 以响应从 JavaScript.

发送给您的消息

步骤 #4:将 WebMessagePort[] 中的第一个元素传递给 Java 脚本,方法是将其包装在 WebMessage 中并在 WebView 上调用 postWebMessage() =]. postWebMessage()Uri 作为第二个参数,并且此 Uri 必须派生自您在步骤 #1 中用作基数 URL 的相同 URL loadDataWithBaseURL().

  @TargetApi(Build.VERSION_CODES.M)
  private void initPort() {
    final WebMessagePort[] channel=wv.createWebMessageChannel();

    port=channel[0];
    port.setWebMessageCallback(new WebMessagePort.WebMessageCallback() {
      @Override
      public void onMessage(WebMessagePort port, WebMessage message) {
        postLux();
      }
    });

    wv.postWebMessage(new WebMessage("", new WebMessagePort[]{channel[1]}),
          Uri.parse(THIS_IS_STUPID));
  }

(其中 wvWebViewTHIS_IS_STUPID 是与 loadDataWithBaseURL() 一起使用的 URL)

第 5 步:您的 Java 脚本可以为全局 onmessage 事件分配一个函数,该函数将在调用 postWebMessage() 时调用。您在事件中获得的 ports 数组的第 0 个元素将是通信管道的 Java 脚本端,您可以将其填充到某处的变量中。如果需要,您可以为该端口分配一个函数给 onmessage,如果 Java 代码将使用 WebMessagePort 发送未来的数据。

第 6 步:当您想从 JavaScript 向 Java 发送消息时,在第 5 步的端口上调用 postMessage(),该消息将被传递到您在步骤 #3 中使用 setWebMessageCallback() 注册的回调。

var port;

function pull() {
    port.postMessage("ping");
}

onmessage = function (e) {
    port = e.ports[0];

    port.onmessage = function (f) {
        parse(f.data);
    }
}

This sample app demonstrates the technique. It has a WebView that shows the current light level based on the ambient light sensor. That sensor data is fed into the WebView either on a push basis (as the sensor changes) or on a pull basis (user taps the "Light Level" label on the Web page). This app uses WebMessagePort for these on Android 6.0+ devices, though the push option is commented out so you can confirm that the pull approach is working through the port. I will have more detailed coverage of the sample app in an upcoming edition of my book.

@CommonsWare 我已经尝试过您的解决方案,它对我有用。只是一点点补充。您也可以使用 loadUrl(),方法是将 Uri 参数设置为 Uri.EMPTY。在 Nexus 7 (MOB30J) 上工作。

    getWebView().postWebMessage(new WebMessage("MESSAGE", new WebMessagePort[]{
            channel[1]
    }), Uri.EMPTY);

这是一个使用兼容库的解决方案: Download Full Solution in Android Studio format

此示例使用存储在资产文件夹中的 index.html 和 index.js 文件。

这是JS:

const channel = new MessageChannel();
var nativeJsPortOne = channel.port1;
var nativeJsPortTwo = channel.port2;
window.addEventListener('message', function(event) {
    if (event.data != 'capturePort') {
        nativeJsPortOne.postMessage(event.data)
    } else if (event.data == 'capturePort') {
        /* The following three lines form Android class 'WebViewCallBackDemo' capture the port and assign it to nativeJsPortTwo
        var destPort = arrayOf(nativeToJsPorts[1])
        nativeToJsPorts[0].setWebMessageCallback(nativeToJs!!)
        WebViewCompat.postWebMessage(webView, WebMessageCompat("capturePort", destPort), Uri.EMPTY) */
        if (event.ports[0] != null) {
            nativeJsPortTwo = event.ports[0]
        }
    }
}, false);

nativeJsPortOne.addEventListener('message', function(event) {
    alert(event.data);
}, false);

nativeJsPortTwo.addEventListener('message', function(event) {
    alert(event.data);
}, false);
nativeJsPortOne.start();
nativeJsPortTwo.start();

这里是 HTML:

<!DOCTYPE html>
<html lang="en-gb">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebView Callback Demo</title>
    <script src="js/index.js"></script>
</head>
<body>
    <div style="font-size: 24pt; text-align: center;">
        <input type="button" value="Test" onclick="nativeJsPortTwo.postMessage(msgFromJS.value);" style="font-size: inherit;" /><br />
        <input id="msgFromJS" type="text" value="JavaScript To Native" style="font-size: 16pt; text-align: inherit; width: 80%;" />
    </div>
</body>
</html>

最后这是本机 Android 代码:

class PostMessageHandler(webView: WebView) {
    private val nativeToJsPorts = WebViewCompat.createWebMessageChannel(webView)
    private var nativeToJs: WebMessagePortCompat.WebMessageCallbackCompat? = null
    init {
        if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_CALLBACK_ON_MESSAGE)) {
            nativeToJs = object : WebMessagePortCompat.WebMessageCallbackCompat() {
                override fun onMessage(port: WebMessagePortCompat, message: WebMessageCompat?) {
                    super.onMessage(port, message)
                    Toast.makeText(webView.context, message!!.data, Toast.LENGTH_SHORT).show()
                }
            }
        }
        var destPort = arrayOf(nativeToJsPorts[1])
        nativeToJsPorts[0].setWebMessageCallback(nativeToJs!!)
        WebViewCompat.postWebMessage(webView, WebMessageCompat("capturePort", destPort), Uri.EMPTY)
    }
}

从 'WebViewClient.onPageFinished(webView: WebView, url: String)' 回调中执行本机代码很重要。 有关完整详细信息,请参阅上面的下载 link。这个项目展示了 postMessage 的两种工作方式(Native to JS 和 JS to Native) 希望这有帮助。