【问题标题】:How Do You Use WebMessagePort As An Alternative to addJavascriptInterface()?您如何使用 WebMessagePort 作为 addJavascriptInterface() 的替代方法?
【发布时间】:2017-06-04 19:40:38
【问题描述】:

Google 为 Android 应用开发者提供的 security guidelines 具有以下功能:

WebViews 不使用 addJavaScriptInterface() 处理不受信任的内容。

在 Android M 及以上版本中,可以使用 HTML 消息通道。

据我所知,“HTML 消息通道”指的是 createWebMessageChannel()WebMessagePortWebMessage 和 kin 之类的东西。

但是,它们没有提供任何示例。他们所做的只是链接到WhatWG specification,这还不清楚。而且,基于对createWebMessageChannel 的 Google 搜索,它似乎还没有被太多使用——我的 blog post describing changes in the Android 6.0 SDK 进入前 10 名搜索结果,我只是顺便提一下。

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

【问题讨论】:

  • 什么是“不受信任的内容”?与 https 相比,通过 http 提供的内容?
  • @Nilzor:我认为“不受信任的内容”是指“不是你自己写的东西”。 IOW、Web 浏览器、RSS 提要阅读器和类似类型的应用程序不应使用addJavascriptInterface()

标签: android android-webview


【解决方案1】:

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

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

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

步骤#3:当您想要初始化这些通信时,请在WebView 上调用createWebMessageChannel(),以创建WebMessagePort[]。该数组中的第 0 个元素是通信管道的末端,您可以在其上调用 setWebMessageCallback() 以便能够响应从 JavaScript 发送给您的消息。

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

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

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

var port;

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

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

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

This sample app 演示了该技术。它有一个WebView,它根据环境光传感器显示当前的光照水平。该传感器数据以推送方式(随着传感器变化)或拉取方式(用户点击网页上的“Light Level”标签)输入WebView。这个应用程序在 Android 6.0+ 设备上使用WebMessagePort 来处理这些,尽管推送选项已被注释掉,因此您可以确认拉取方法正在通过端口工作。我将在即将发布的my book 版本中更详细地介绍示例应用程序。

【讨论】:

  • 对于 6.0 之前的 Android 版本是否会有 WebMessageChannel 的支持版本?
  • @Gohan:我对此表示怀疑,尽管对于 Android 4.4+ 来说这不是不可能的。我的猜测是 Android System WebView 应用程序具有 Web 端。我怀疑没有好的方法可以创建一个支持库,以某种方式使 Java 端工作。早于 4.4 的情况不太可能发生,因为我怀疑这些版本中的 WebView 是否对此有任何支持。但是,谷歌的方式很神秘,所以,¯\_(ツ)_/¯
  • 感谢您进行所有相关研究并分享!您是否比较了使用它而不是 @JavascriptInterface 处理二进制数据的性能?
  • @BenjaminGruenbaum" 好的,所以听起来他们在某处搞砸了 byte[]-passing 代码。很高兴知道 - 谢谢!
  • @AdamBurley:正确!我在the sample project's JavaScript 中找到了它。我已经修复了答案以匹配它。谢谢!
【解决方案2】:

在 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。 至少有一个起点。

【讨论】:

    【解决方案3】:

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

    此示例使用存储在 assets 文件夹中的 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)' 回调执行本机代码很重要。 有关完整详细信息,请参阅上面的下载链接。这个项目显示 postMessage 双向工作(原生到 JS 和 JS 到原生) 希望这会有所帮助。

    【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 这几乎更可怕。我认为Uri 参数背后的意义在于确保我将消息发布到预期的页面。无论如何,很高兴知道 - 谢谢!
    • 真的是必须先完全加载页面才能打开 WebMessagingChannel 吗? postMessage API 中没有提到这一点。
    • @Tom 抱歉回答迟了。我记不太清了,但听上去很有道理。
    • @user2319066 是的,我找到了一些确认信息。然后,WebMessagingChannel 对我的目的毫无用处:(
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-11
    相关资源
    最近更新 更多