如何使用 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()
填充您的 WebView
。 loadUrl()
将不起作用,因为 bugs。您需要使用 http
或 https
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));
}
(其中 wv
是 WebView
,THIS_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)
希望这有帮助。
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()
填充您的 WebView
。 loadUrl()
将不起作用,因为 bugs。您需要使用 http
或 https
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));
}
(其中 wv
是 WebView
,THIS_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) 希望这有帮助。