# launch_handler 清单成员

> launch_handler 成员的 client_mode 决定再次启动已安装的 Web 应用时是聚焦现有窗口、让它导航，还是新开一个窗口；Chrome 与 Edge 110 实现了它。

`launch_handler` 告诉浏览器：已安装的 Web 应用再次被启动时（来自系统启动器、快捷方式、文件关联、
协议链接或分享），该怎样处理它已经打开的窗口。它唯一的属性 `client_mode` 在三种做法之间选择：
聚焦已打开的窗口、让该窗口导航到启动 URL，或新开一个窗口。

Chrome 110、Edge 110、Samsung Internet 21.0 与 Android WebView 110 实现了该成员（BCD
`html.manifest.launch_handler`）。Firefox 157 与 Safari 27 解析后丢弃，所以在那里每次启动都会
新开窗口或标签页。启动 URL 通过 `window.launchQueue` 送达页面，`focus-existing` 正是靠它才有用。

## 成员

- **类型**：对象，只有一个属性 `client_mode`，取字符串或字符串数组。允许的值：`auto`、
  `navigate-new`、`navigate-existing`、`focus-existing`。
- **默认值**：`auto`。成员缺省、`client_mode` 不是字符串或数组、数组里没有任何可识别的值，
  这三种情况都解析为 `auto`。
- **示例值**：`{ "client_mode": ["focus-existing", "auto"] }`。

已有窗口打开时，各模式的行为：

- `auto` 把选择权交给浏览器。Chromium 在 Android 上取 `navigate-existing`（单实例是常态），
  在桌面端取 `navigate-new`。
- `navigate-new` 在启动 URL 处新开一个应用窗口。
- `navigate-existing` 把最近使用的窗口提到前台，并让它导航到启动 URL。
- `focus-existing` 把该窗口提到前台但不导航。页面通过 `launchQueue` 收到启动 URL，自行决定
  怎么处理。

没有窗口打开时，`focus-existing` 与 `navigate-existing` 的表现等同于 `navigate-new`。对于数组，
浏览器取第一个它认得的值，因此清单可以优先 `focus-existing`，同时为将来新增模式的引擎写上 `auto`。

Chromium 解析器对形状很严格：非对象的值产生 `launch_handler value ignored, object expected.`，
`client_mode` 中未知的字符串产生 `client_mode value '<value>' ignored, unknown value.`
（`manifest_parser.cc` 的 `ParseLaunchHandler`）。被跳过的只是无效条目，不是整个成员。

:::observed
Chrome 155（macOS 26，英文界面）的 DevTools > Application > Manifest：清单写
`"launch_handler": { "client_mode": "focus" }`（`focus-existing` 的误拼）时，**Errors and
warnings** 下多出 `client_mode value 'focus' ignored, unknown value.`，从 Dock 再次启动已安装
应用会打开第二个窗口，这是桌面端 `auto` 的行为。把值改为 `focus-existing` 后这一行消失，再次启动
变为把现有窗口提到前台。
:::

## 示例

清单一侧只有一行；有意思的代码在 `launchQueue` 消费者里，是它让 `focus-existing` 真正做事。

### 保持单一编辑器窗口并把启动导入其中

一个文档编辑器只想要一个窗口，每次启动切换到被请求的文档，而不是堆叠窗口。清单请求
`focus-existing`；页面消费启动 URL 并更新自身状态。

```json
{
  "name": "Draft",
  "start_url": "/",
  "display": "standalone",
  "launch_handler": { "client_mode": ["focus-existing", "auto"] },
  "file_handlers": [
    { "action": "/open", "accept": { "text/markdown": [".md"] } }
  ]
}
```

```js
if ('launchQueue' in window) {
  window.launchQueue.setConsumer(async (launchParams) => {
    const url = new URL(launchParams.targetURL);
    if (launchParams.files.length > 0) {
      const file = await launchParams.files[0].getFile();
      await openDocument(file);
      return;
    }
    const doc = url.searchParams.get('doc');
    if (doc) await openDocumentById(doc);
  });
} else {
  // 没有 launchQueue：浏览器做的是普通导航，直接读 URL 即可。
  const doc = new URL(location.href).searchParams.get('doc');
  if (doc) await openDocumentById(doc);
}
```

消费者会为创建该窗口的那次启动运行一次，之后每一次被导入该窗口的启动再运行一次。启动来自文件
关联时，`launchParams.files` 是 `FileSystemFileHandle` 数组，否则为空。

### 检测该 API 并回退到普通导航

`launchQueue` 是整套机制的检测点。没有它的浏览器会把每次启动都变成到 `targetURL` 的普通导航，
所以回退就是读 `location`。下面的辅助函数让应用其余部分无论哪种情况都只面对一个回调。

```js
export function onLaunch(handler) {
  if ('launchQueue' in window) {
    window.launchQueue.setConsumer((params) => handler(new URL(params.targetURL)));
  } else {
    handler(new URL(location.href));
  }
}

onLaunch((url) => {
  if (url.pathname === '/share') showShareSheet(url.searchParams);
});
```

在模块的第一个 `await` 之前注册消费者：Chromium 会把启动参数排队到有消费者为止，晚注册的
消费者仍会被触发，所以不会丢，只会延迟。

### 桌面端偏好新窗口、移动端偏好现有窗口

一个新闻阅读器希望桌面端每次启动都在自己的窗口里，而在手机上只保留一个窗口。`auto` 在
Chromium 里已经编码了这种区分，清单把它显式写出来，把选择交给浏览器。

```json
{
  "launch_handler": { "client_mode": "auto" }
}
```

写出 `auto` 与省略该成员等价；这个值存在的意义是让日后修改清单时有一个明显的位置可改。

## 另请参阅

- [file_handlers 清单成员](/zh/reference/manifest/file-handlers/)
- [protocol_handlers 清单成员](/zh/reference/manifest/protocol-handlers/)
- [share_target 清单成员](/zh/reference/manifest/share-target/)
- [start_url 清单成员](/zh/reference/manifest/start-url/)
- [Web App Launch Handler API: launch_handler member](https://wicg.github.io/web-app-launch/#launch_handler-member)（wicg.github.io）
- [Control how your app is launched](https://developer.chrome.com/docs/web-platform/launch-handler)（developer.chrome.com）
- [LaunchQueue](https://developer.mozilla.org/en-US/docs/Web/API/LaunchQueue)（developer.mozilla.org）