TWA 的 PostMessage

Sayed El-Abady
Sayed El-Abady

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

学完本指南后,您将能够:

  • 了解客户端和 Web 内容验证的工作原理。
  • 了解如何初始化客户端和 Web 内容之间的通信通道。
  • 了解如何向 Webcontent 发送消息以及从 Webcontent 接收消息。

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

  • 将最新的 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 API 允许两个有效来源相互通信,即来源和目标来源。为了让 Android 应用能够向目标源发送消息,它需要声明其等效的源源。为此,您可以使用 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";

// The origin the TWA is equivalent to, where the Digital Asset Links file
// was created with the "use_as_origin" relationship.
private Uri SOURCE_ORIGIN = Uri.parse("https://source-origin.example.com");

// The origin the TWA will communicate with. In most cases, SOURCE_ORIGIN and
// TARGET_ORIGIN will be the same.
private Uri TARGET_ORIGIN = Uri.parse("https://target-origin.example.com");

// It stores the validation result so you can check on it before requesting
// postMessage channel, since without successful validation it is not possible
// 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