Contact Picker API 需要每次请求时同意,而不仅是用户信任
如何安全地在 PWA 中实施联系人选择且不存储联系人数据。
为什么联系人选择器对 PWA 很重要
联系人选择器(Contact Picker)让 PWA 能访问用户设备上的联系人,主要用于:
- 消息应用:选择联系人发送邮件或聊天消息
- 音视频通话:检索联系人电话号码发起 VoIP 通话
- 社交网络:发现已加入平台的联系人
- 协作工具:快速邀请同事加入工作区
MDN 文档强调,此 API 仅在安全顶级浏览上下文中可用,并非常小心地考虑联系人数据的敏感性和隐私。
关键设计原则:访问联系人无持久性,用户必须每次请求时授予权限。
如何实施联系人选择器
步骤 1:功能检测
检查浏览器是否支持 Contact Picker API:
const supported = 'contacts' in navigator;
if (!supported) {
// 提供替代方案:手动输入联系方式、上传 CSV、或跳过此功能
showFallbackUI();
return;
}步骤 2:检查支持的属性
设备可能支持部分联系人属性(如仅名称和电话,不支持地址):
async function getSupportedProperties() {
const supportedProperties = await navigator.contacts.getProperties();
return {
name: supportedProperties.includes('name'),
email: supportedProperties.includes('email'),
tel: supportedProperties.includes('tel'),
address: supportedProperties.includes('address'),
icon: supportedProperties.includes('icon')
};
}步骤 3:请求联系人
使用 navigator.contacts.select() 显示联系人选择器:
const props = ['name', 'email', 'tel'];
const opts = { multiple: true };
async function selectContacts() {
try {
const contacts = await navigator.contacts.select(props, opts);
return contacts;
} catch (ex) {
console.error('Contact picker failed:', ex);
return null;
}
}关键参数:
props:要请求的属性数组(name、email、tel、address、icon)opts.multiple:是否允许多选联系人
返回的 contacts 数组每个元素包含选中的联系人数据:
[{ name: 'John Doe', email: 'john@example.com', tel: '555-1234' }, ...]步骤 4:处理选中的联系人
async function inviteContacts() {
try {
const contacts = await selectContacts();
if (!contacts || contacts.length === 0) {
return;
}
// 使用联系人数据
for (const contact of contacts) {
if (contact.email) {
sendInvitationEmail(contact.email);
}
if (contact.tel) {
sendSMSInvitation(contact.tel);
}
}
// 重要:不存储联系人数据
console.log(`Invited ${contacts.length} contacts`);
} catch (ex) {
showError('Failed to invite contacts');
}
}隐私设计原则
默认不存储
MDN 文档明确指出:
访问联系人无持久性;用户必须每次请求时授予权限。
这意味着:
- ❌ 不缓存联系人列表到 localStorage
- ❌ 不上传联系人到服务器(除非用户明确同意)
- ❌ 不分析联系人数据用于推荐
- ❌ 不构建社交图谱
正确做法:
- 仅在当前会话中临时使用联系人数据
- 处理完成后立即清除引用
- 需要长期保存时(如邀请列表),只保存用户明确同意保存的项目(如用户手动添加的电话号码,而非从通讯录导入)
最小化数据请求
只请求你真正需要的属性:
// ❌ 坏:请求所有可用属性,过度收集不必要数据
const allProps = ['name', 'email', 'tel', 'address', 'icon'];
navigator.contacts.select(allProps, { multiple: true });
// ✅ 好:只请求语音通话需要的属性
const voiceCallProps = ['name', 'tel'];
navigator.contacts.select(voiceCallProps, { multiple: false });用户控制权
- 每次请求都需要用户确认:浏览器显示联系人选择器,用户必须主动选择联系人
- 单选 vs 多选:根据用例设置
opts.multiple - 清晰的目的说明:按钮文本应解释为何访问联系人(如"邀请同事加入")
浏览器兼容性检查清单
在产品化前:
- [ ] 检查
navigator.contacts存在性 - [ ] 测试支持的属性(
getProperties()) - [ ] 测试用户拒绝权限时的行为
- [ ] 测试空联系人列表场景
- [ ] 测试联系人数据格式(可选字段可能缺失)
兼容性现状(根据 MDN):
- Chrome Desktop: 部分支持
- Chrome Android: 完全支持
- Edge Desktop/Android: Chrome 同行
- Safari iOS/Desktop: 不支持
- Firefox: 不支持
实施检查清单
在发布前:
技术实现
- [ ] 功能检测并提供回退方案
- [ ] 异常处理覆盖所有失败场景
- [ ] 清晰的 UI 反馈(成功/失败/取消)
- [ ] 不假设任何联系人属性一定存在
用户体验
- [ ] 按钮标签明确说明用途("邀请联系人" vs "选择联系人")
- [ ] 提供手动输入联系方式的替代方案
- [ ] 显示已选择的联系人摘要(不泄露完整数据)
- [ ] 提供"撤销邀请"功能(如果保存了邀请记录)
隐私合规
- [ ] 点击触发(非页面加载时自动请求)
- [ ] 隐私政策说明 Contact Picker 使用
- [ ] 不存储或上传未明示的联系人数据
- [ ] 提供清除已使用联系人历史的方法
对开发者意味着什么
产品机会
- 协作工具 PWA:快速加入联系人,提升转化
- 社交网络:发现已注册用户,引导注册
- Messaging 应用:自然选择通信对象
技术债务
- 兼容性:需要为不支持浏览器提供备选方案
- UI 实现:联系人选择器由浏览器控制,无法自定义外观
- 调试困难:联系人数据在真实设备上测试才有意义
用户体验风险
- 权限疲劳:频繁请求联系人权限可能被用户拒绝
- 信任问题:用户可能怀疑 PWA 试图"窃取"通讯录
- 跨场景限制:仅顶级页面可用,iframe 无法使用
决策框架
| 场景 | 使用 Contact Picker | 不使用 Contact Picker | |------|---------------------|----------------------| | 内部协作工具 | ✅ 提升入职效率 | ⚠️ 依靠邮件/链接邀请 | | 社交网络邀请 | ✅ 发现已注册用户 | ⚠️ 改用搜索手机号/邮箱 | | 客服聊天 | ⚠️ 可选性功能 | ✅ 用户直接选择服务 | | e-commerce | ❌ 无关联场景 | ✅ 无需联系人信息 | | 游戏 | ❌ 无关联场景 | ✅ 社交功能不依赖通讯录 | | 医疗/金融 | ⚠️ 需额外合规审查 | ✅ 避免接触敏感数据 |
下一步
- 需求审查:确定你的 PWA 真的需要访问联系人,还是可以通过其他方式完成任务
- 隐私审查:确保不会存储或上传联系人数据,除非用户明确授权
- 原型测试:在 Chrome Android 上测试联系人选择器流程
- 用户教育:在首次使用时清晰解释为何需要联系人,增强信任
联系人选择器是强大的能力,但也是极度敏感的隐私入口。透明、克制、尊重用户,才能避免信任危机。