# registerProtocolHandler() 与 manifest 协议处理程序

> navigator.registerProtocolHandler() 与 manifest 的 protocol_handlers 成员共用一套规则：web+ 协议名单、%s 占位符，以及 SecurityError 与 SyntaxError 的触发条件。

把 Web 应用注册为 `mailto:` 或 `web+music:` 这类 URL 协议的处理程序，有两条路。`navigator.registerProtocolHandler(scheme, url)` 由页面发起、向用户弹出询问，在普通标签页里就能用；manifest 的 `protocol_handlers` 成员在 PWA 安装时向操作系统注册，不需要页面发起任何提示。两者都执行 HTML 标准的「normalize protocol handler parameters」步骤，所以协议与 URL 的规则完全一致：方法会抛异常的值，在 manifest 里会被静默丢弃。

`registerProtocolHandler()` 在桌面端 Firefox 2 与 Chrome 13 起就有，Edge 79 与 Opera 11.6 跟进；Chrome 77 起 `url` 只接受 `http:` 与 `https:`。Safari 没有实现，Android 上没有任何浏览器暴露它（BCD `api.Navigator.registerProtocolHandler`）。`unregisterProtocolHandler()` 只存在于 Chromium，始于 Chrome 38。manifest 成员是 Chrome 96 与 Edge 96 的桌面专属功能，完整支持情况见兼容性表。

## 语法

```js
navigator.registerProtocolHandler(scheme, url)
navigator.unregisterProtocolHandler(scheme, url)
```

```json
{
  "protocol_handlers": [
    { "protocol": "web+music", "url": "/play?song=%s" }
  ]
}
```

两个方法都返回 `undefined`。注册在调用返回后并行进行，没有可等待的 Promise，也无法得知用户是否接受了提示。两者都带 `[SecureContext]`，`url` 的同源要求本身也蕴含了这一点。激活带已注册协议的链接时，浏览器把完整链接做百分号编码、替换掉 `%s`，然后在新的顶层浏览上下文中导航到结果。

## 参数

方法参数与 manifest 条目成员一一对应。

| 方法参数 | manifest 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| `scheme` | `protocol` | `DOMString` | 是 | 检查前先转小写。必须是安全名单内的协议，或以 `web+` 开头后接一个或多个小写 ASCII 字母（`web+music` 可以，`web+music2` 与 `web+` 不行）。末尾带冒号即无效。 |
| `url` | `url` | `DOMString` | 是 | 必须包含字面量 `%s`。方法按文档解析、成员按 manifest URL 解析；结果必须是文档同源的 `http:` 或 `https:` URL，manifest 还要求位于其 `scope` 之内。 |

安全名单内的协议是 `bitcoin`、`ftp`、`ftps`、`geo`、`im`、`irc`、`ircs`、`magnet`、`mailto`、`matrix`、`mms`、`news`、`nntp`、`openpgp4fpr`、`sftp`、`sip`、`sms`、`smsto`、`ssh`、`tel`、`urn`、`webcal`、`wtai` 与 `xmpp`。manifest 处理时会跳过缺少 `protocol` 或 `url`、规范化失败、超出 scope，或 `url` 与前面条目重复的项，并且不报告原因。

## 异常

`registerProtocolHandler()` 与 `unregisterProtocolHandler()` 同步抛出。manifest 成员不抛任何异常，无效条目在处理阶段被丢弃。

| 异常 | 条件 |
|---|---|
| `SecurityError` | `scheme` 既不在安全名单内，也不是 `web+` 加小写字母，包括带冒号的 `"mailto:"`；解析后的 `url` 不是 `http:` 或 `https:`，或与文档不同源；浏览器拦截了注册，例如注册 `http` 本身。Chrome 还会在 Isolated Web App 中抛出，并提示改用 manifest 成员。 |
| `SyntaxError` | `url` 不含 `%s`；`url` 解析失败，`%s` 位于 host 或端口时必然如此。 |

:::observed
Chrome 会在异常消息里说明是哪项检查失败。`navigator.registerProtocolHandler("music", "/play?song=%s")` 抛出 `SecurityError: Failed to execute 'registerProtocolHandler' on 'Navigator': The scheme 'music' doesn't belong to the scheme allowlist. Please prefix non-allowlisted schemes with the string 'web+'.`；不含占位符的 `url` 抛出 `SyntaxError: Failed to execute 'registerProtocolHandler' on 'Navigator': The url provided ('/play') does not contain '%s'.`；指向另一个源的处理程序抛出 `SecurityError: ... Can only register custom handler in the document's origin.`。消息在 Chromium 的 [`navigator_content_utils.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/navigatorcontentutils/navigator_content_utils.cc) 中拼装（chromium.googlesource.com）。
:::

## 示例

示例为部署在 `https://player.example/` 的播放器注册 `web+music`。处理路由是 `/play?song=%s`，点击 `web+music:track/42` 会在该源打开 `/play`，`song` 参数经 `URLSearchParams` 解码后仍是 `web+music:track/42`。

### 在设置页提供注册入口并给出手动回退

先检测方法是否存在，再同时捕获两个异常名：这里的 `SecurityError` 多半是协议名写错，而用户拒绝提示时页面根本不会知道。Safari 和所有 Android 浏览器走回退分支，向用户说明如何改为粘贴链接。

```js
const button = document.querySelector("#register-handler");

if (typeof navigator.registerProtocolHandler !== "function") {
  button.hidden = true;
  document.querySelector("#paste-link-help").hidden = false;
} else {
  button.addEventListener("click", () => {
    try {
      navigator.registerProtocolHandler("web+music", "/play?song=%s");
      button.textContent = "已请求，请在浏览器提示中确认";
    } catch (err) {
      if (err.name === "SecurityError" || err.name === "SyntaxError") {
        console.error(`${err.name}: ${err.message}`);
      } else {
        throw err;
      }
    }
  });
}
```

HTML 标准要求浏览器记住被拒绝的注册、不再重复提示，所以拒绝后再点一次可能毫无反应；在按钮的帮助文字里把这一点说清楚。

### 在 manifest 中声明同一处理程序并读取启动 URL

manifest 带上该条目后，已安装的用户无需提示即可获得关联。路由在 `song` 查询参数里收到整条链接（含协议），需要自己去掉前缀。

```json
{
  "name": "Player",
  "start_url": "/",
  "scope": "/",
  "protocol_handlers": [
    { "protocol": "web+music", "url": "/play?song=%s" }
  ]
}
```

```js
const launched = new URL(location.href).searchParams.get("song");

if (launched && launched.startsWith("web+music:")) {
  const trackId = launched.slice("web+music:".length); // "track/42"
  loadTrack(trackId);
} else {
  showLibrary(); // 直接打开，而不是通过协议链接
}
```

路由要放在 manifest 的 `scope` 之内：超出 scope 的 `url` 会从处理后的 manifest 中被丢弃，安装照常完成，却没有处理程序，也没有任何警告。

### 移除用户不再需要的处理程序

`unregisterProtocolHandler()` 接受同样的两个参数，只存在于 Chromium。要和 `registerProtocolHandler()` 分开检测，因为 Firefox 有前者没有后者。

```js
function forgetHandler() {
  if (typeof navigator.unregisterProtocolHandler !== "function") {
    return false; // Firefox：只能在浏览器自己的设置里移除
  }
  navigator.unregisterProtocolHandler("web+music", "/play?song=%s");
  return true;
}
```

通过方法取消注册不影响 manifest 建立的关联；那一份随已安装应用一起移除。

## 另请参阅

- [Manifest protocol_handlers](/zh/reference/manifest/protocol-handlers/)，该成员自己的条目，含处理细节
- [Manifest 文件处理程序（file_handlers）](/zh/reference/manifest/file-handlers/)，针对文件类型的同类操作系统注册
- [处理文件](/zh/guides/file-handling/)
- [HTML Standard: registerProtocolHandler() method](https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator-registerprotocolhandler)（html.spec.whatwg.org）
- [HTML Standard: safelisted schemes](https://html.spec.whatwg.org/multipage/system-state.html#safelisted-scheme)（html.spec.whatwg.org）
- [Web App Manifest incubations: protocol_handlers member](https://wicg.github.io/manifest-incubations/#protocol_handlers-member)（wicg.github.io）
- [URL protocol handler registration for PWAs](https://developer.chrome.com/docs/web-platform/best-practices/url-protocol-handler)（developer.chrome.com）
- [Handle protocols in Progressive Web Apps](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps/how-to/handle-protocols)（learn.microsoft.com）