能力 · API
Web NFC API
发布于 更新于
NDEFReader 与贴近手机的标签交换 NFC Data Exchange Format(NDEF)消息:scan() 开始监听,每当标签进入范围就触发带标签序列号和记录的 reading 事件;write() 把一条消息写入下一个靠近的标签;makeReadOnly() 把标签永久锁定。PWA 可以用它做库存标签、活动签到,或与贴有 NFC 贴纸的设备配对;NDEF 之外的底层标签协议不对外暴露。
Android 版 Chrome 89 是唯一的实现:它提供 NDEFReader、scan()、write() 以及 reading 与 readingerror 事件,Android 版 Chrome 100 加入 makeReadOnly()(BCD api.NDEFReader)。跟随 Chrome 的 Android Chromium 浏览器(如 Samsung Internet)继承该支持;桌面 Chrome、Firefox 与 Safari 均无支持,WebKit 已提交「反对」立场(standards-positions #584)。
const reader = new NDEFReader()
reader.scan()reader.scan(options)reader.write(message)reader.write(message, options)reader.makeReadOnly()reader.makeReadOnly(options)三个方法都返回 Promise<undefined>。scan() 在监听开始时即兑现,而不是在读到标签时;结果通过 reading(NDEFReadingEvent,带 serialNumber 与 message)和 readingerror(标签在范围内但无法读取时触发)送达。write() 与 makeReadOnly() 在标签写入或锁定完成后兑现,没有标签靠近时可能要等上数秒。接口标注为 [SecureContext, Exposed=Window],只能在顶层浏览上下文中使用,页面不可见时会被挂起。
scan() 与 makeReadOnly() 各接受一个可选字典;write() 接受一条消息和一个可选字典。
| 方法 | 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
scan() |
options |
NDEFScanOptions { signal } |
否 | signal(AbortSignal)停止监听,并把该 reader 从活动集合中移除。 |
write() |
message |
NDEFMessageSource:DOMString、BufferSource 或 NDEFMessageInit |
是 | 字符串变成一条 "text" 记录,缓冲区变成一条类型为 application/octet-stream 的 "mime" 记录,NDEFMessageInit 则显式给出记录。 |
write() |
options |
NDEFWriteOptions { overwrite = true, signal } |
否 | overwrite: false 时,标签上已有 NDEF 记录则拒绝写入。signal 中止挂起的写入。 |
makeReadOnly() |
options |
NDEFMakeReadOnlyOptions { signal } |
否 | signal 中止挂起的锁定。 |
NDEFMessageInit 只有一个必填成员 records,类型为 sequence<NDEFRecordInit>。
NDEFRecordInit 成员 |
类型 | 必填 | 说明 |
|---|---|---|---|
recordType |
USVString |
是 | "empty"、"text"、"url"、"smart-poster"、"mime"、"absolute-url"、"unknown",形如 "example.com:shelf" 的外部类型,或智能海报内部形如 ":act" 的本地类型。 |
mediaType |
USVString |
仅 "mime" |
载荷的 MIME 类型;其他记录类型都不得携带。 |
id |
USVString |
否 | 记录标识符;"empty" 记录不得携带。 |
encoding |
USVString |
否 | 仅用于 "text":"utf-8"(默认)、"utf-16"、"utf-16be" 或 "utf-16le"。 |
lang |
USVString |
否 | "text" 记录的 BCP 47 语言标签,如 "zh";默认取文档语言。 |
data |
any |
视类型而定 | "text"、"url"、"absolute-url" 用 DOMString;"mime"、"unknown" 用 BufferSource;外部类型与 "smart-poster" 用 BufferSource 或 NDEFMessageInit;"empty" 省略。 |
NDEFMessageInit 的嵌套最多 32 层,外部类型名不得超过 255 字节。
在返回 Promise 之前执行的检查会立即拒绝;其余检查发生在用户授予 nfc 权限之后。
| 异常 | 方法 | 条件 |
|---|---|---|
InvalidStateError |
全部 | 调用方不是活动的顶层浏览上下文(iframe 被拒),或对 scan() 而言该 reader 已在扫描中。 |
信号的 abort reason(默认 AbortError) |
全部 | 调用时 options.signal 已中止,或操作挂起期间被中止。 |
TypeError |
write() |
消息无效:records 为空;嵌套超过 32 层;禁止携带 mediaType 或 id 的记录类型带了它们;data 类型与 recordType 不匹配;"text" 的 encoding 不在允许的四种之内;"url" 无法解析;外部类型名超过 255 字节;或 "smart-poster" 没有恰好一条 URL 记录。 |
NotAllowedError |
全部 | 用户拒绝了 nfc 权限,或(overwrite: false 的 write())标签上已有 NDEF 记录。 |
NotSupportedError |
全部 | 设备没有 NFC 适配器或无法连接适配器,适配器不支持推送数据,或标签不暴露 NDEF 技术且无法格式化。 |
NotReadableError |
全部 | 浏览器不被允许使用适配器,例如系统设置中已关闭 NFC。 |
NetworkError |
write()、makeReadOnly() |
向标签传输或锁定操作失败,例如标签在写入途中被移开。 |
scan() 本身不会报告无法读取的标签:那会以不带载荷的 readingerror 事件到达 reader,处理函数应提示用户稳住标签再试一次。
每个示例都先检测 window 中是否有 NDEFReader;在桌面浏览器和 iOS 上会走回退分支,通常是携带同一标识符的二维码或手动输入。
扫描标签,缺少支持时回退到二维码
Section titled “扫描标签,缺少支持时回退到二维码”从一次点击开始扫描,让权限提示与手势绑定;解码 "text" 与 "url" 记录,没有 Web NFC 时显示二维码扫描器。scan() 在监听开始时兑现,真正的读取稍后通过 reading 事件到达。
async function startTagScan(onTag, showQrFallback) { if (!('NDEFReader' in window)) { showQrFallback(); return; }
const reader = new NDEFReader(); reader.addEventListener('reading', ({ serialNumber, message }) => { const decoder = new TextDecoder(); for (const record of message.records) { if (record.recordType === 'text') onTag({ serialNumber, text: decoder.decode(record.data) }); if (record.recordType === 'url') onTag({ serialNumber, url: decoder.decode(record.data) }); } }); reader.addEventListener('readingerror', () => onTag({ error: '标签无法读取,请稳住标签再试一次。' }));
try { await reader.scan(); } catch (err) { if (err.name === 'NotAllowedError') showQrFallback(); else throw err; }}record.data 是 DataView;"text" 记录在 record.encoding 中携带编码、在 record.lang 中携带语言,所以 UTF-16 标签要用 new TextDecoder(record.encoding) 而不是默认值。
只向空白标签写入 URL,并设置超时
Section titled “只向空白标签写入 URL,并设置超时”overwrite: false 保护已有数据的标签,AbortSignal 在十秒内没有标签靠近时停止挂起的写入。没有 Web NFC 时函数返回 false,界面可以改为提供打印二维码标签的选项。
async function writeLabel(url) { if (!('NDEFReader' in window)) return false;
const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 10_000); try { await new NDEFReader().write( { records: [{ recordType: 'url', data: url }] }, { overwrite: false, signal: controller.signal }, ); return true; } catch (err) { if (err.name === 'AbortError') return false; // 10 秒内没有标签 if (err.name === 'NotAllowedError') throw new Error('标签已有内容,或 NFC 权限被拒绝'); throw err; } finally { clearTimeout(timer); }}标签在传输途中移开会以 NetworkError 拒绝;此时标签上的记录可能只写了一部分,重试应使用 overwrite: true。
写入厂商记录并锁定标签
Section titled “写入厂商记录并锁定标签”外部记录类型让应用在自己的命名空间下存放结构化数据。写入后 makeReadOnly() 锁定标签,访客的手机就无法改动它;锁定不可逆,所以示例先请求确认。
async function provisionShelfTag(shelfId, confirmLock) { if (!('NDEFReader' in window)) return 'unsupported';
const reader = new NDEFReader(); const payload = new TextEncoder().encode(JSON.stringify({ shelfId })); await reader.write({ records: [{ recordType: 'example.com:shelf', data: payload }] });
if (!(await confirmLock())) return 'written'; try { await reader.makeReadOnly(); return 'locked'; } catch (err) { if (err.name === 'NotSupportedError') return 'written-not-lockable'; throw err; }}外部类型 example.com:shelf 必须全小写、使用你控制的域名并保持在 255 字节以内;读回时 recordType === 'example.com:shelf',record.data 中是同样的字节。
- Web Bluetooth API,用于通过无线广播而非携带标签的设备
- WebHID API
- WebUSB API
- Web NFC: NDEFReader scan() method(w3c-cg.github.io)
- Web NFC: security policies(w3c-cg.github.io)
- WebKit standards position: Web NFC(github.com)
- Interact with NFC devices on Chrome for Android(developer.chrome.com)
规范
| 规范 | 状态 |
|---|---|
| Web NFC API | 社区草案 |
| Web NFC: NDEFReader scan() method | 社区草案 |
| Web NFC: NDEFReader write() method | 社区草案 |
| Web NFC: NDEFReader makeReadOnly() method | 社区草案 |
- 图例
- 支持
- 部分支持
- 需开启标志
- 不支持
- 未知
| 浏览器 / 平台 | 支持 | 版本 | 置信度 | 来源 | 备注 |
|---|---|---|---|---|---|
| Chrome (Desktop) | 不支持 | — | 高 | 来源 | 1 |
| Chrome (Android) | 支持 | 89 | 高 | 来源 | — |
| Edge (Desktop) | 不支持 | — | 高 | 来源 | 23 |
| Firefox (Desktop) | 不支持 | — | 高 | 来源 | 4 |
| Firefox (Android) | 不支持 | — | 高 | 来源 | 56 |
| Safari (macOS) | 不支持 | — | 高 | 来源 | 7 |
| Safari (iOS) | 不支持 | — | 高 | 来源 | 89 |
| Samsung Internet | 支持 | 15.0 | 高 | 来源 | 10 |
| WebView (Android) | 支持 | 89 | 高 | 来源 | 11 |
- browser-compat-data 未记录 Chrome 的支持。
- browser-compat-data 未记录 Edge 的支持。
- 由 browser-compat-data 镜像自 Chrome 的数据推导。
- browser-compat-data 未记录 Firefox 的支持。
- browser-compat-data 未记录 Firefox for Android 的支持。
- 由 browser-compat-data 镜像自 Firefox 的数据推导。
- browser-compat-data 未记录 Safari 的支持。
- browser-compat-data 未记录 iOS 版 Safari 的支持。
- 由 browser-compat-data 镜像自 Safari 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。
- 由 browser-compat-data 镜像自 Chrome Android 的数据推导。