# Meta Quest 上的 PWA

> 介绍面向 Meta Horizon OS 的 Web 应用如何使用 Bubblewrap 与 Trusted Web Activity，并检测 WebXR 和保留非沉浸式回退。

Meta Horizon OS 支持可在 Meta Horizon Store 分发的 Web 版 2D 体验。Meta 文档把 Bubblewrap 定义为将 PWA 打包成 Android App Bundle 的工具，包内使用 Trusted Web Activity 打开 Web 内容（[Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/)，Meta Horizon OS Developers）。

## 平台支持

Meta 文档说明，Web 版 2D 体验可在 Meta Horizon OS 上通过 Meta Horizon Store 分发。

沉浸式功能还要考虑 WebXR 的可用性：MDN 将 `XRSystem.isSessionSupported()` 标为有限可用，并说明支持它的浏览器只在安全上下文中开放此方法；该方法用于检查用户的 WebXR 设备是否支持请求的会话模式（[Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/)，Meta Horizon OS Developers；[XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported)，MDN）。

## 工作原理

Trusted Web Activity 让 Android 应用通过基于 Custom Tabs 的协议打开 PWA 内容。Digital Asset Links 验证 Android 应用和网站是否出自同一开发者，页面仍由用户的浏览器渲染（[Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/)，Chrome for Developers）。

:::caution
`xr-spatial-tracking` 权限策略阻止 WebXR 时，`isSessionSupported()` 返回的 Promise 会以 `SecurityError` 拒绝。代码既要处理 `false` 结果，也要处理拒绝（[XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported)，MDN）。
:::

## 示例

浏览器没有报告沉浸式 VR 会话支持时，示例仍显示普通页面内容。

### 检测支持后显示沉浸式控件

将这个完整页面部署到 HTTPS 来源。代码检测 `navigator.xr`，处理不支持和 Promise 拒绝两条路径，并保留非沉浸式内容。

```html
<!doctype html>
<html lang="zh-CN">
  <body>
    <p>无需沉浸式会话也能使用此内容。</p>
    <button id="enter-xr" hidden>进入 XR</button>
    <output id="xr-status">正在检查 WebXR 支持……</output>
    <script>
      const button = document.querySelector('#enter-xr');
      const status = document.querySelector('#xr-status');

      if (!('xr' in navigator)) {
        status.value = '继续使用非沉浸模式。';
      } else {
        navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
          button.hidden = !supported;
          status.value = supported
            ? '可以使用沉浸模式。'
            : '继续使用非沉浸模式。';
        }).catch(() => {
          button.hidden = true;
          status.value = '继续使用非沉浸模式。';
        });
      }
    </script>
  </body>
</html>
```

`navigator.xr` 缺失、请求的会话模式不受支持或检测发生拒绝时，按钮都会保持隐藏。

### 从 URL 读取启动数据

Trusted Web Activity 宿主可以在 URL 中向 Web 内容传递数据，查询参数也是可用方式（[Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/)，Chrome for Developers）。将以下页面保存为 `launch.html`，再打开 `launch.html?source=quest`。

```html
<!doctype html>
<html lang="zh-CN">
  <body>
    <output id="launch-source">未提供启动来源。</output>
    <script>
      const source = new URLSearchParams(location.search).get('source');
      if (source) {
        document.querySelector('#launch-source').value = `启动来源：${source}。`;
      }
    </script>
  </body>
</html>
```

查询参数存在时，页面显示 `启动来源：quest。`；参数缺失时，页面保留回退文本。

## 另请参阅

- [Manifest scope](/zh/reference/manifest/scope/)
- [可安装的 PWA](/zh/guides/installable/)
- [Start Building with Meta Quest 3S](https://developers.meta.com/vr/blog/start-building-meta-quest-3s-3-launch-spatial-sdk-2D-mixed-reality/)（Meta Horizon OS Developers）
- [Trusted Web Activity overview](https://developer.chrome.com/docs/android/trusted-web-activity/)（Chrome for Developers）
- [XRSystem: isSessionSupported() method](https://developer.mozilla.org/en-US/docs/Web/API/XRSystem/isSessionSupported)（MDN）