# Contact Picker API

> navigator.contacts.select() 打开由浏览器绘制的选择器，只返回用户选中的联系人与属性。本页列出成员、异常、各版本支持情况与回退方案。

Contact Picker API 即 `navigator.contacts`，在浏览器自己绘制的选择器中请用户从设备通讯录挑选条目，只把用户同意分享的联系人及属性（`name`、`email`、`tel`、`address`、`icon`）交给页面。没有批量读取，也没有持久授权：每次调用都会再次显示选择器。

Android 上的 Chrome 80（Android 6 及以上）是唯一的稳定实现；`address` 与 `icon` 在 Chrome 84 加入。桌面 Chrome、Edge 与 Firefox 不暴露 `navigator.contacts`。iOS 14.5 的 Safari 把它放在「设置 > Safari 浏览器 > 高级」下名为「Contact Picker API」的实验性功能开关之后（默认关闭）；Samsung Internet 14.0 至 21 暴露了接口，但打开选择器时失败（BCD `api.ContactsManager`）。MDN 将该 API 标记为实验性且不属于 Baseline。

## 语法

```js
navigator.contacts.select(properties)
navigator.contacts.select(properties, options)
navigator.contacts.getProperties()
```

`select()` 返回 `Promise<sequence<ContactInfo>>`，用户取消时以空数组兑现。`getProperties()` 返回 `Promise<sequence<ContactProperty>>`，列出设备能提供的属性。两者都在只暴露于 `Window` 的 `[SecureContext]` 接口上；`select()` 还要求顶层浏览上下文与瞬时用户激活，`getProperties()` 两者都不要求。

## 参数

`select()` 接受要请求的属性列表和一个选项；`getProperties()` 不接受参数。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `properties` | `sequence<ContactProperty>` | 是 | `"name"`、`"email"`、`"tel"`、`"address"`、`"icon"` 中的一个或多个。空列表会拒绝；浏览器不支持的值会拒绝。 |
| `options.multiple` | `boolean` | 否 | 默认 `false`：选择器只允许选一个联系人。`true` 允许多选。 |

兑现的每个 `ContactInfo` 只携带被请求的成员，且每个成员都是序列，因为一个联系人可能有多个值：

| 成员 | 类型 | 出现条件 |
|---|---|---|
| `name` | `sequence<DOMString>` | 请求了 `"name"` |
| `email` | `sequence<DOMString>` | 请求了 `"email"` |
| `tel` | `sequence<DOMString>` | 请求了 `"tel"` |
| `address` | `sequence<ContactAddress>` | 请求了 `"address"`；`ContactAddress` 与 `PaymentAddress` 同形（`country`、`addressLine`、`region`、`city`、`postalCode` 等） |
| `icon` | `sequence<Blob>` | 请求了 `"icon"` |

用户在选择器里选择不分享的成员，与联系人本来就没有的成员无法区分：两者都返回空序列。

## 异常

`select()` 以三个名称之一拒绝；`getProperties()` 不会拒绝。

| 异常 | 条件 |
|---|---|
| `InvalidStateError` | 调用方不是顶层 traversable（例如在 iframe 中）；该 navigable 已有选择器在显示；或呈现选择器、读取联系人源失败。 |
| `SecurityError` | 调用时没有瞬时用户激活。 |
| `TypeError` | `properties` 为空，或包含联系人源支持集合之外的属性（Chrome 80 至 83 中的 `"address"` 与 `"icon"`）。 |

Chromium 在最后一种 `InvalidStateError` 情形上有偏差：Android 选择器无法启动时，它以 `TypeError: Unable to open a contact selector` 拒绝。

:::observed
在 Android 的 Chrome 中，从 `setTimeout` 回调运行 `navigator.contacts.select(['name'])`，以 `SecurityError: A user gesture is required to call this method` 拒绝；同一调用放在 iframe 里以 `InvalidStateError: The contacts API can only be used in the top frame` 拒绝，面板未关闭时再次调用以 `InvalidStateError: Contacts Picker is already in use.` 拒绝。英文界面的 Android 选择器确认按钮文字为 **Done**。三条字符串来自 Chromium 的 [`contacts_manager.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/contacts_picker/contacts_manager.cc)（chromium.googlesource.com）。
:::

## 示例

两个示例都用 Chrome 文档给出的 `'contacts' in navigator && 'ContactsManager' in window` 做特性检测，并回退到普通表单字段，因为该 API 在所有桌面浏览器以及未开启功能开关的 iOS Safari 上都不存在。

### 选一个收件人邮箱，缺少支持时改为手动输入

在 `click` 处理函数内调用 `select()`，把空结果当作取消而非错误。API 缺失时显示 `<input type="email">`，而不是隐藏整个功能。

```js
const pickButton = document.querySelector('#pick-recipient');
const manualField = document.querySelector('#recipient-email');

const supported = 'contacts' in navigator && 'ContactsManager' in window;
pickButton.hidden = !supported;
manualField.hidden = supported;

pickButton.addEventListener('click', async () => {
  try {
    const [contact] = await navigator.contacts.select(['name', 'email']);
    if (!contact) return; // 用户取消
    const email = contact.email[0];
    if (!email) {
      manualField.hidden = false; // 该联系人没有邮箱
      return;
    }
    manualField.value = email;
  } catch (err) {
    console.error(`${err.name}: ${err.message}`);
    manualField.hidden = false;
  }
});
```

即使选择成功，读取 `contact.email[0]` 也可能得到 `undefined`，所以手动输入框作为最后手段保留在 DOM 中，而不是被移除。

### 按设备能返回的属性裁剪请求

`getProperties()` 在你请求之前告诉你 `address` 或 `icon` 是否可用；请求不受支持的属性会让整个调用以 `TypeError` 拒绝。先过滤期望列表，再打开多选选择器。

```js
async function pickAttendees(wanted = ['name', 'tel', 'icon']) {
  if (!('contacts' in navigator && 'ContactsManager' in window)) {
    return null; // 调用方显示自己的参会人表单
  }
  const available = await navigator.contacts.getProperties();
  const props = wanted.filter((p) => available.includes(p));
  if (props.length === 0) return null;

  const contacts = await navigator.contacts.select(props, { multiple: true });
  return contacts.map((c) => ({
    name: c.name?.[0] ?? '',
    tel: c.tel?.[0] ?? '',
    avatar: c.icon?.[0] ? URL.createObjectURL(c.icon[0]) : null,
  }));
}
```

`getProperties()` 不需要用户手势，可以在页面加载时运行并决定渲染哪些按钮；只有 `select()` 调用必须等待点击。

## 另请参阅

- [Web Share API](/zh/reference/capabilities/web-share/)，PWA 能从手势中打开的另一个系统面板
- [Payment Request API](/zh/reference/capabilities/payment-request/)，`ContactAddress` 复用了它的 `PaymentAddress` 结构
- [Contact Picker API: select() method](https://w3c.github.io/contact-picker/#contacts-manager-select)（w3.org）
- [A contact picker for the web](https://developer.chrome.com/docs/capabilities/web-apis/contact-picker)（developer.chrome.com）
- [Chrome Platform Status: Contacts API](https://chromestatus.com/feature/6511327140904960)（chromestatus.com）