# Manifest protocol_handlers

> protocol_handlers 清单成员把 Web 应用注册进操作系统的应用偏好设置，成为 mailto、web+jngl 等 URL 协议方案的处理程序。

import CompatTable from '@components/CompatTable.astro';

按 MDN，`protocol_handlers` 成员"指定一个对象数组，这些对象是该 Web 应用可以注册并处理的协
议"，并且"协议处理程序会把该应用注册进操作系统的应用偏好设置；这项注册把一个特定应用与给定
的协议方案关联起来"。MDN 自己给的说明是："当在网页上使用 `mailto://` 协议处理程序时，已注册
的邮件应用会被打开。"

## 支持情况

<CompatTable feature="protocol-handlers" />

MDN 把该成员标记为**实验性**。其 `manifests.webapp.protocol_handlers` 的
browser-compat-data 条目记录 Chrome 96 起支持，Edge 与 Opera 被标记为镜像 Chrome 的数据。
Firefox 与 Safari 记录为不支持，Android 上的 Chrome 同样记录为不支持；其余移动端条目
（Firefox for Android、Opera Android、iOS 上的 Safari、三星浏览器，以及 Android 与 iOS 的
WebView）记录为对上述引擎的镜像。

## 如何使用

每个条目把一个 `protocol` 与一个 `url` 配成一对。按 MDN，`protocol` 是"一个必需的字符串，内
容为要处理的协议；例如：`mailto`、`ms-word`、`web+jngl`"，而 `url` 是"位于应用 `scope` 之内
的必需 HTTPS URL，用于处理该协议"，其中"`%s` 令牌会被替换为以该协议处理程序的方案开头的
URL"。MDN 还补充："如果 `url` 是相对 URL，基准 URL 就是清单的 URL。"

MDN 的示例声明该应用"应被注册为处理 `web+jngl` 与 `web+jnglstore` 协议"：

```json
{
  "protocol_handlers": [
    {
      "protocol": "web+jngl",
      "url": "/lookup?type=%s"
    },
    {
      "protocol": "web+jnglstore",
      "url": "/shop?for=%s"
    }
  ]
}
```

MDN 描述了接下来发生的事："在把 Web 应用注册为协议处理程序之后，当用户从浏览器或原生应用中
点击带有特定方案（例如 `mailto://` 或 `web+music://`）的超链接时，已注册的 PWA 就会打开并接
收该 URL。"

Manifest Incubations 规范把这件事拆成两个角色。在安装时，"用户代理 SHOULD 向操作系统注册协
议处理程序"；此后"点击一个已注册的协议会启动已注册的应用。如果它就是这个 Web 应用，那么执行
[HTML] 中定义的 [invoke a protocol handler] 步骤，其中用户代理 SHOULD 导航到 url，并把相应
的浏览上下文设为一个新的顶级浏览上下文"。当多个应用声明同一协议时，规范预期"由操作系统让用
户选择应由哪个应用打开，并且也允许用户设置默认应用"。

规范自带的示例把替换过程说得很具体：对于位于
`https://example.com/manifest.webmanifest` 的清单，一个 `web+music` 处理程序配
`"url": "/play?songId=%s"`，在 `web+music://#1234` 上被激活时，"用户代理会实例化一个新的顶
级浏览上下文，并导航到 `https://example.com/play?songId=web+music://%231234`"。

## 如何检测被替换的 URL 并回退

按 MDN，`%s` 会被替换为"以该协议处理程序的方案开头的 URL"。所引用的来源没有把未经替换的
模板定义为回退 URL，因此该路由应当只接受文档所述的替换后形状，并让缺失或无效的值进入回退分
支：

```js
// 清单指向的路由：/lookup?type=%s
//
// 注意这里的解析方式。MDN："URLSearchParams 构造函数把加号（+）解释为空格，
// 这可能会带来问题。"而在规范的示例结果中，"web+music" 的 "+" 在查询字符串里是
// 字面保留的，所以要读取原始值。
const encoded = location.search.match(/[?&]type=([^&]*)/)?.[1];
let value = null;

try {
  value = encoded ? decodeURIComponent(encoded) : null;
} catch {
  // 未经替换或格式错误的值，不是已处理的协议 URL。
}

if (value?.startsWith('web+jngl:')) {
  // 这个值具有 MDN 所记录的、%s 被替换后的形状。
  showLookupFor(value.slice('web+jngl:'.length));
} else {
  // 缺失，或者不是 web+jngl URL。渲染带自身搜索框的普通查询页，
  // 而不是去解析一个并不存在的值。
  showLookupForm();
}
```

## 实践清单

- [ ] 按 MDN，把处理程序的 `url` 保持在应用 `scope` 之内并使用 HTTPS。规范的处理步骤会跳过
      任何归一化后 URL"不在清单 scope 之内"的条目，也会跳过 `protocol` 或 `url` 为 undefined
      的条目。
- [ ] 不要指望任意方案都会被接受。在规范的示例中，第二个处理程序"会被忽略，因为所提供的协议
      既不以 `web+` 开头，也不属于安全名单内的方案"。
- [ ] 不要把同一个 URL 注册两次：按规范的处理步骤，如果已处理列表中已包含该归一化 URL，该条
      目会被跳过。
- [ ] 权限提示并非必然出现，而呈现的列表可能被截短。按规范，"用户代理 SHOULD 在把某个协议
      处理程序描述 `protocol_handlers` 注册为宿主操作系统中该协议的默认处理程序之前，先向用
      户请求许可"，并且"MAY 截短该列表……以便与宿主操作系统的惯例或限制保持一致"。
- [ ] 把注册当作依赖操作系统的事，而且它并不只发生在安装时。按 MDN，"把应用注册为处理 URL 方
      案是依赖操作系统的。这项关联通常在应用安装期间完成，但也可以在之后从一个已安装的应用中
      完成。"
- [ ] 解析整个 URL，而不是某个载荷。`%s` 被替换为"以该协议处理程序的方案开头的 URL"（MDN）
      ——在规范的示例里，查询值是完整的 `web+music://#1234`，其中片段标记写作 `%23`。
- [ ] 注意这项注册可能在无声中变成默认。规范的隐私说明提醒："取决于操作系统的能力，协议处理
      程序可能在用户并不明确知情的情况下成为某个协议的『默认』处理程序"，并列出了用户代理可
      以采取的保护措施，包括移除该注册。

## 延伸阅读

- [Manifest protocol_handlers：为自定义 URL 方案注册 PWA](/zh/reference/capabilities/protocol-handlers/)
  ——能力视角的条目，包含更早的命令式注册 API。
- [Manifest Protocol Handlers 支持情况](/zh/compatibility/protocol-handlers/)——该成员的兼容
  性数据集。
- [Manifest scope](/zh/reference/manifest/scope/)——界定规范会接受哪些处理程序 URL 的成员。