与网页内容交互¶
与网页内容和 WebExtensions 交互¶
GeckoView 允许嵌入式应用程序在 GeckoView 实例中注册和运行 WebExtensions。扩展程序是与网页内容交互的首选方式。
在 GeckoView 中运行扩展程序¶
与应用程序捆绑的扩展程序可以放在 APK 的 /assets
部分中的一个文件夹中。与普通扩展程序一样,每个与 GeckoView 捆绑的扩展程序都需要一个 manifest.json 文件。
为了定位与 APK 捆绑的文件,GeckoView 提供了一个简写 resource://android/
,它指向 APK 的根目录。
例如,resource://android/assets/messaging/
将指向 APK 中存在的 /assets/messaging/
文件夹。
注意:每个已安装的扩展程序都需要在 manifest.json
文件中指定一个 id 和 version。
要在 GeckoView 中安装捆绑的扩展程序,只需调用 WebExtensionController.installBuiltIn。
runtime.getWebExtensionController()
.installBuiltIn("resource://android/assets/messaging/")
请注意,扩展程序的生命周期与 GeckoRuntime 实例的生命周期无关。即使应用程序重新启动,扩展程序也会持续存在。每次启动时安装都可以,但速度可能会很慢。为了避免多次安装,您可以使用 WebExtensionRuntime.ensureBuiltIn
,它仅在扩展程序尚未安装时才会安装。
runtime.getWebExtensionController()
.ensureBuiltIn("resource://android/assets/messaging/", "[email protected]")
.accept(
extension -> Log.i("MessageDelegate", "Extension installed: " + extension),
e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
);
与网页内容通信¶
GeckoView 允许通过扩展程序与网页进行双向通信。
使用 GeckoView 时,可以使用 本地消息传递 来与浏览器进行通信。
runtime.sendNativeMessage 用于一次性消息。
runtime.connectNative 用于基于连接的消息传递。
注意:只有当扩展程序的 manifest.json
文件中存在 geckoViewAddons
权限 时,这些 API 才可用。
一次性消息¶
从 内容脚本 或 后台脚本 发送消息的最简单方法是使用 runtime.sendNativeMessage。
注意:通常,原生扩展程序会使用 原生清单 来定义要使用的原生应用程序标识符。对于 GeckoView,这 *不需要*,setMessageDelegate
中的 nativeApp
参数将用于确定使用什么原生应用程序字符串。
消息传递示例¶
要接收来自后台脚本的消息,请在 WebExtension 对象上调用 setMessageDelegate。
extension.setMessageDelegate(messageDelegate, "browser");
SessionController.setMessageDelegate 允许应用程序接收来自内容脚本的消息。
session.getWebExtensionController()
.setMessageDelegate(extension, messageDelegate, "browser");
注意:上面的代码中的 "browser"
参数决定了扩展程序可以使用什么原生应用程序 ID 来发送本地消息。
注意:扩展程序只能在应用程序通过在 manifest.json 文件中添加 nativeMessagingFromContent
明确授权的情况下,才能从内容脚本发送消息,例如:
"permissions": [
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons"
]
示例¶
让我们设置一个活动,该活动注册位于 APK 的 /assets/messaging/
文件夹中的扩展程序。此活动将设置一个 MessageDelegate,该委托将用于与网页内容通信。
您可以在此处找到完整的示例:MessagingExample。
Activity.java¶
WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
@Nullable
public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
final @NonNull Object message,
final @NonNull WebExtension.MessageSender sender) {
// The sender object contains information about the session that
// originated this message and can be used to validate that the message
// has been sent from the expected location.
// Be careful when handling the type of message as it depends on what
// type of object was sent from the WebExtension script.
if (message instanceof JSONObject) {
// Do something with message
}
return null;
}
};
// Let's make sure the extension is installed
runtime.getWebExtensionController()
.ensureBuiltIn(EXTENSION_LOCATION, "[email protected]").accept(
// Set delegate that will receive messages coming from this extension.
extension -> session.getWebExtensionController()
.setMessageDelegate(extension, messageDelegate, "browser"),
// Something bad happened, let's log an error
e -> Log.e("MessageDelegate", "Error registering extension", e)
);
现在,将 geckoViewAddons
、nativeMessaging
和 nativeMessagingFromContent
权限添加到您的 manifest.json
文件中。
/assets/messaging/manifest.json¶
{
"manifest_version": 2,
"name": "messaging",
"version": "1.0",
"description": "Example messaging web extension.",
"browser_specific_settings": {
"gecko": {
"id": "[email protected]"
}
},
"content_scripts": [
{
"matches": ["*://*.twitter.com/*"],
"js": ["messaging.js"]
}
],
"permissions": [
"nativeMessaging",
"nativeMessagingFromContent",
"geckoViewAddons"
]
}
最后,编写一个内容脚本,该脚本将在发生特定事件时向应用程序发送消息。例如,您可以在页面上找到 WPA 清单 时发送消息。请注意,我们用于 sendNativeMessage
的 nativeApp
标识符与 Activity.java 中 setMessageDelegate
调用的标识符相同。
/assets/messaging/messaging.js¶
let manifest = document.querySelector("head > link[rel=manifest]");
if (manifest) {
fetch(manifest.href)
.then(response => response.json())
.then(json => {
let message = {type: "WPAManifest", manifest: json};
browser.runtime.sendNativeMessage("browser", message);
});
}
您可以在上面 MessageDelegate 中的 onMessage
方法中处理此消息。
@Nullable
public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
final @NonNull Object message,
final @NonNull WebExtension.MessageSender sender) {
if (message instanceof JSONObject) {
JSONObject json = (JSONObject) message;
try {
if (json.has("type") && "WPAManifest".equals(json.getString("type"))) {
JSONObject manifest = json.getJSONObject("manifest");
Log.d("MessageDelegate", "Found WPA manifest: " + manifest);
}
} catch (JSONException ex) {
Log.e("MessageDelegate", "Invalid manifest", ex);
}
}
return null;
}
请注意,在内容脚本的情况下,sender.session
将是消息来源的 GeckoSession
实例的引用。对于后台脚本,sender.session
将始终为 null
。
还要注意,message
的类型将取决于从扩展程序发送的内容。
当扩展程序发送 JavaScript 对象时,message
的类型将为 JSONObject
,但如果扩展程序发送一个基本类型,例如:
runtime.browser.sendNativeMessage("browser", "Hello World!");
则 message
的类型将为 java.util.String
。
基于连接的消息传递¶
对于更复杂的情况或当您想 *从* 应用程序向扩展程序发送消息时,runtime.connectNative 是合适的 API。
connectNative
返回一个 runtime.Port,可用于向应用程序发送消息。在应用程序端,实现 MessageDelegate#onConnect 将允许您接收一个 Port 对象,该对象可用于接收和发送消息到扩展程序。
以下示例可以在 此处 找到。
对于此示例,扩展程序端将执行以下操作
使用
connectNative
在后台脚本上打开一个端口侦听端口并在控制台中记录接收到的每条消息
在打开端口后立即发送一条消息。
/assets/messaging/background.js¶
// Establish connection with app
let port = browser.runtime.connectNative("browser");
port.onMessage.addListener(response => {
// Let's just echo the message back
port.postMessage(`Received: ${JSON.stringify(response)}`);
});
port.postMessage("Hello from WebExtension!");
在应用程序端,遵循上面 示例,onConnect
将把 Port
对象存储在一个成员变量中,然后在需要时使用它。
private WebExtension.Port mPort;
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... initialize GeckoView
// This delegate will handle all communications from and to a specific Port
// object
WebExtension.PortDelegate portDelegate = new WebExtension.PortDelegate() {
public WebExtension.Port port = null;
public void onPortMessage(final @NonNull Object message,
final @NonNull WebExtension.Port port) {
// This method will be called every time a message is sent from the
// extension through this port. For now, let's just log a
// message.
Log.d("PortDelegate", "Received message from WebExtension: "
+ message);
}
public void onDisconnect(final @NonNull WebExtension.Port port) {
// After this method is called, this port is not usable anymore.
if (port == mPort) {
mPort = null;
}
}
};
// This delegate will handle requests to open a port coming from the
// extension
WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
@Nullable
public void onConnect(final @NonNull WebExtension.Port port) {
// Let's store the Port object in a member variable so it can be
// used later to exchange messages with the WebExtension.
mPort = port;
// Registering the delegate will allow us to receive messages sent
// through this port.
mPort.setDelegate(portDelegate);
}
};
runtime.getWebExtensionController()
.ensureBuiltIn("resource://android/assets/messaging/", "[email protected]")
.accept(
// Register message delegate for background script
extension -> extension.setMessageDelegate(messageDelegate, "browser"),
e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
);
// ... other
}
例如,让我们在用户长按虚拟键盘上的某个键(例如后退按钮)时向扩展程序发送一条消息。
@Override
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
if (mPort == null) {
// No extension registered yet, let's ignore this message
return false;
}
JSONObject message = new JSONObject();
try {
message.put("keyCode", keyCode);
message.put("event", KeyEvent.keyCodeToString(event.getKeyCode()));
} catch (JSONException ex) {
throw new RuntimeException(ex);
}
mPort.postMessage(message);
return true;
}
这允许应用程序和扩展程序之间的双向通信。