TWA 的 PostMessage

Sayed El-Abady
Sayed El-Abady

从 Chrome 115 起,Trusted Web Activity (TWA) 可以使用 postMessage 发送消息。本文介绍了在您的应用与 Web 之间进行通信所需的设置。

学完本指南后,您将能够: - 了解客户端验证和 Web 内容验证的工作原理。 - 了解如何初始化客户端和 Web 内容之间的通信通道。 - 了解如何向 Web 内容发送消息和接收来自 Web 内容的消息。

要遵照本指南操作,您需要:

  • 将最新的 androidx.browser(最低 v1.6.0-alpha02)库添加到 build.gradle 文件中。
  • 适用于 TWA 的 Chrome 版本 115.0.5790.13 或更高版本。

window.postMessage() 方法可以安全地实现 Window 对象之间的跨源通信。例如,在网页与其生成的弹出式窗口之间,或在网页与内嵌于其中嵌入的 iframe 之间。

通常情况下,只有当不同网页上的脚本来自同一来源、共用相同的协议、端口号和主机(也称为“同源政策”)时,这些脚本才能相互访问。window.postMessage() 方法提供了一种受控机制,用于在不同源之间安全通信。这对于实现聊天应用、协作工具和其他工具非常有用。例如,聊天应用可以使用 postMessage 在位于不同网站的用户之间发送消息。在 Trusted Web Activity (TWA) 中使用 postMessage 可能有点复杂,本指南逐步介绍了如何在 TWA 客户端中使用 postMessage 向网页发送消息和接收来自网页的消息。

将应用添加到网站验证中

为了让 postMessage 正常运行,它要求网站和启动此网站的 Trusted Web Activity 应用之间建立有效关系,为此,可以使用 Digital Asset Links (DAL) 实现,方法是在 assetlinks.json 文件中添加应用的 package name,并将其与 use_as_origin 相关联,如下所示:

[{
  "relation": ["delegate_permission/common.use_as_origin"],
  "target" : { "namespace": "android_app", "package_name": "com.example.app", "sha256_cert_fingerprints": [""] }
}]

请注意,在与 TWA 关联的来源上设置时,必须为 MessageEvent.origin 字段提供一个来源,但 postMessage 可用于与不包含 Digital Asset Links 的其他网站通信。例如,如果您拥有 www.example.com,则必须通过 DAL 证明这一点,但您可以与其他任何网站(例如 www.wikipedia.org)进行通信。

将 PostMessageService 添加到清单中

如需接收 postMessage 通信,您需要设置服务,只需在 Android 清单中添加 PostMessageService 即可:

<service android:name="androidx.browser.customtabs.PostMessageService"
android:exported="true"/>

获取 CustomTabsSession 实例

将服务添加到清单后,使用 CustomTabsClient 类绑定该服务。连接后,您可以使用提供的客户端创建新会话,如下所示。 CustomTabsSession 是用于处理 postMessage API 的核心类。以下代码展示了在连接服务后,如何使用客户端创建新会话,此会话将用于 postMessage

private CustomTabsClient mClient;
private CustomTabsSession mSession;

// We use this helper method to return the preferred package to use for
// Custom Tabs.
String packageName = CustomTabsClient.getPackageName(this, null);

// Binding the service to (packageName).
CustomTabsClient.bindCustomTabsService(this, packageName, new CustomTabsServiceConnection() {
 @Override
 public void onCustomTabsServiceConnected(@NonNull ComponentName name,
     @NonNull CustomTabsClient client) {
   mClient = client;

   // Note: validateRelationship requires warmup to have been called.
   client.warmup(0L);

   mSession = mClient.newSession(customTabsCallback);
 }

 @Override
 public void onServiceDisconnected(ComponentName componentName) {
   mClient = null;
 }
});

现在,您想知道这个 customTabsCallback 实例是什么,对吗?我们将在下一部分中创建该 API。

创建 CustomTabsCallback

CustomTabsCallback 是一个回调类,CustomTabsClient 用于获取与其自定义标签页中事件相关的消息。其中一项事件是 onPostMessage,当应用收到来自 Web 的消息时,系统会调用此事件。向客户端添加回调以初始化 postMessage 通道,以开始通信,如以下代码所示。

private final String TAG = "TWA/CCT-PostMessageDemo";
private Uri SOURCE_ORIGIN = Uri.parse("my-app-origin-uri");
private Uri TARGET_ORIGIN = Uri.parse("website-you-are-communicating-with");

// It stores the validation result so you can check on it before requesting postMessage channel, since without successful validation it is not posible to use postMessage.
boolean mValidated;

CustomTabsCallback customTabsCallback = new CustomTabsCallback() {

// Listens for the validation result, you can use this for any kind of
// logging purposes.
 @Override
 public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin,
     boolean result, @Nullable Bundle extras) {
   // If this fails:
   // - Have you called warmup?
   // - Have you set up Digital Asset Links correctly?
   // - Double check what browser you're using.
   Log.d(TAG, "Relationship result: " + result);
   mValidated = result;
 }

// Listens for any navigation happens, it waits until the navigation finishes
// then requests post message channel using
// CustomTabsSession#requestPostMessageChannel(sourceUri, targetUri, extrasBundle)

// The targetOrigin in requestPostMessageChannel means that you can be certain their messages are delivered only to the website you expect.
 @Override
 public void onNavigationEvent(int navigationEvent, @Nullable Bundle extras) {
   if (navigationEvent != NAVIGATION_FINISHED) {
     return;
   }

   if (!mValidated) {
     Log.d(TAG, "Not starting PostMessage as validation didn't succeed.");
   }

   // If this fails:
   // - Have you included PostMessageService in your AndroidManifest.xml ?
boolean result = mSession.requestPostMessageChannel(SOURCE_ORIGIN, TARGET_ORIGIN, new Bundle());
   Log.d(TAG, "Requested Post Message Channel: " + result);
 }

// This gets called when the channel we requested is ready for sending/receiving messages.
 @Override
 public void onMessageChannelReady(@Nullable Bundle extras) {
   Log.d(TAG, "Message channel ready.");

   int result = mSession.postMessage("First message", null);
   Log.d(TAG, "postMessage returned: " + result);
 }

// Listens for upcoming messages from Web.
 @Override
 public void onPostMessage(@NonNull String message, @Nullable Bundle extras) {
   super.onPostMessage(message, extras);
// Handle the received message.

 }
};

通过网络进行通信

现在我们可以从托管应用发送和接收消息了,我们该如何通过网络实现同样的操作呢?通信必须从托管应用开始,然后网页需要从第一条消息中获取端口。此端口用于传回通信。您的 JavaScript 文件将如以下示例所示:

window.addEventListener("message", function (event) {
  // We are receiveing messages from any origin, you can check of the origin by
  // using event.origin

  // get the port then use it for communication.
  var port = event.ports[0];
  if (typeof port === 'undefined') return;

  // Post message on this port.
  port.postMessage("Test")

  // Receive upcoming messages on this port.
  port.onmessage = function(event) {
    console.log("[PostMessage1] Got message" + event.data);
  };
});

您可以点击此处查看完整示例

照片由 Joanna Kosinska 拍摄,来源:Unsplash