现在可以读取和写入 NFC 标签了。
什么是 Web NFC?
NFC 是近距离无线通信的缩写,是一种工作频率为 13.56 MHz 的短距离无线技术,可在距离小于 10 厘米的设备之间进行通信,传输速率最高可达 424 kbit/s。
Web NFC 使网站能够在 NFC 标签靠近用户设备(通常为 5-10 厘米,2-4 英寸)时读取和写入这些标签。当前范围仅限于 NFC 数据交换格式 (NDEF),这是一种轻量级二进制消息格式,适用于不同的标签格式。

建议的应用场景
Web NFC 仅限于 NDEF,因为读取和写入 NDEF 数据的安全属性更易于量化。不支持低级 I/O 操作(例如 ISO-DEP、NFC-A/B、NFC-F)、对等通信模式和基于主机的卡模拟 (HCE)。
以下是一些可能使用 Web NFC 的网站示例:
- 当用户将设备触碰展品附近的 NFC 卡时,博物馆和美术馆可以显示有关展品的更多信息。
- 库存管理网站可以读取或写入容器上 NFC 标签的数据,以更新有关容器内容的信息。
- 会议网站可以使用它在活动期间扫描 NFC 徽章,并确保徽章已锁定,以防止进一步更改写入其中的信息。
- 网站可以使用它来共享设备或服务配置方案所需的初始密钥,还可以在运行模式下部署配置数据。

当前状态
步骤 | 状态 |
---|---|
1. 创建说明 | 完成 |
2. 创建规范的初始草稿 | 完成 |
3. 收集反馈并迭代设计 | 完成 |
4. 源试用 | 完成 |
5. 启动 | 完成 |
使用 Web NFC
功能检测
硬件的功能检测与您可能习惯的方式不同。 NDEFReader
的存在表明浏览器支持 Web NFC,但不能表明是否存在所需的硬件。特别是,如果硬件缺失,某些调用返回的 promise 将拒绝。在介绍 NDEFReader
时,我会提供详细信息。
if ('NDEFReader' in window) { /* Scan and write NFC tags */ }
术语
NFC 标签是一种被动 NFC 设备,这意味着当有主动 NFC 设备(例如手机)靠近时,它会通过磁感应供电。NFC 标签有多种形式,例如贴纸、信用卡、腕带等。

NDEFReader
对象是 Web NFC 中的入口点,用于公开准备读取和/或写入操作的功能,这些操作会在 NDEF 标签靠近时完成。NDEFReader
中的 NDEF
表示 NFC 数据交换格式,这是由 NFC Forum 标准化的轻量级二进制消息格式。
NDEFReader
对象用于处理来自 NFC 标签的传入 NDEF 消息,以及将 NDEF 消息写入范围内的 NFC 标签。
支持 NDEF 的 NFC 标签就像一张便签。任何人都可以读取该文件,除非该文件是只读的,否则任何人都可以写入该文件。它包含一条 NDEF 消息,该消息封装了一条或多条 NDEF 记录。每条 NDEF 记录都是一个包含数据载荷和关联类型信息的二进制结构。Web NFC 支持以下 NFC Forum 标准化记录类型:空、文本、网址、智能海报、MIME 类型、绝对网址、外部类型、未知类型和本地类型。

扫描 NFC 标签
如需扫描 NFC 标签,请先实例化一个新的 NDEFReader
对象。调用 scan()
会返回一个 promise。如果之前未授予访问权限,系统可能会提示用户。如果满足以下所有条件,相应 promise 将会解析:
- 它仅在响应用户手势(例如触摸手势或鼠标点击)时调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
Promise 解析后,您可以通过事件监听器订阅 reading
事件,从而获取传入的 NDEF 消息。您还应订阅 readingerror
事件,以便在附近出现不兼容的 NFC 标签时收到通知。
const ndef = new NDEFReader(); ndef.scan().then(() => { console.log("Scan started successfully."); ndef.onreadingerror = () => { console.log("Cannot read data from the NFC tag. Try another one?"); }; ndef.onreading = event => { console.log("NDEF message read."); }; }).catch(error => { console.log(`Error! Scan failed to start: ${error}.`); });
当 NFC 标签靠近时,系统会触发 NDEFReadingEvent
事件。它包含两个特有的属性:
serialNumber
表示设备的序列号(例如 00-11-22-33-44-55-66),如果没有序列号,则为空字符串。message
表示存储在 NFC 标签中的 NDEF 消息。
如需读取 NDEF 消息的内容,请遍历 message.records
并根据其 recordType
适当处理其 data
成员。 data
成员公开为 DataView
,因为它允许处理以 UTF-16 编码的数据。
ndef.onreading = event => { const message = event.message; for (const record of message.records) { console.log("Record type: " + record.recordType); console.log("MIME type: " + record.mediaType); console.log("Record id: " + record.id); switch (record.recordType) { case "text": // TODO: Read text record with record data, lang, and encoding. break; case "url": // TODO: Read URL record with record data. break; default: // TODO: Handle other records with record data. } } };
写入 NFC 标签
如需写入 NFC 标签,请先实例化一个新的 NDEFReader
对象。调用 write()
会返回一个 promise。如果之前未授予访问权限,系统可能会提示用户。此时,系统会“准备”一个 NDEF 消息,如果满足以下所有条件,Promise 将会解析:
- 它仅在响应用户手势(例如触摸手势或鼠标点击)时调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
- 用户已点按 NFC 标签,并且已成功写入 NDEF 消息。
如需将文本写入 NFC 标签,请将字符串传递给 write()
方法。
const ndef = new NDEFReader(); ndef.write( "Hello World" ).then(() => { console.log("Message written."); }).catch(error => { console.log(`Write failed :-( try again: ${error}.`); });
如需将网址记录写入 NFC 标签,请将表示 NDEF 消息的字典传递给 write()
。在下面的示例中,NDEF 消息是一个包含 records
键的字典。其值是一个记录数组,在本例中是一个网址记录,定义为一个具有 recordType
键(设置为 "url"
)和 data
键(设置为网址字符串)的对象。
const ndef = new NDEFReader(); ndef.write({ records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }] }).then(() => { console.log("Message written."); }).catch(error => { console.log(`Write failed :-( try again: ${error}.`); });
您还可以将多条记录写入 NFC 标签。
const ndef = new NDEFReader(); ndef.write({ records: [ { recordType: "url", data: "https://w3c.github.io/web-nfc/" }, { recordType: "url", data: "https://web.dev/nfc/" } ]}).then(() => { console.log("Message written."); }).catch(error => { console.log(`Write failed :-( try again: ${error}.`); });
如果 NFC 标记包含不应被覆盖的 NDEF 消息,请在传递给 write()
方法的选项中将 overwrite
属性设置为 false
。在这种情况下,如果 NFC 标签中已存储 NDEF 消息,则返回的 promise 将被拒绝。
const ndef = new NDEFReader(); ndef.write("Writing data on an empty NFC tag is fun!", { overwrite: false }) .then(() => { console.log("Message written."); }).catch(error => { console.log(`Write failed :-( try again: ${error}.`); });
将 NFC 标签设为只读
为防止恶意用户覆盖 NFC 标签的内容,您可以将 NFC 标签永久设为只读。此操作是单向过程,无法撤消。NFC 标签一旦设为只读,便无法再写入数据。
如需将 NFC 标签设为只读,请先实例化新的 NDEFReader
对象。调用 makeReadOnly()
会返回一个 promise。如果之前未授予访问权限,系统可能会提示用户。如果满足以下所有条件,相应 promise 将会解析:
- 它仅在响应用户手势(例如触摸手势或鼠标点击)时调用。
- 用户已允许网站与 NFC 设备互动。
- 用户的手机支持 NFC。
- 用户已在手机上启用 NFC。
- 用户已触碰 NFC 标签,并且 NFC 标签已成功设为只读。
const ndef = new NDEFReader(); ndef.makeReadOnly() .then(() => { console.log("NFC tag has been made permanently read-only."); }).catch(error => { console.log(`Operation failed: ${error}`); });
以下介绍了如何在向 NFC 标签写入数据后将其永久设置为只读。
const ndef = new NDEFReader(); try { await ndef.write("Hello world"); console.log("Message written."); await ndef.makeReadOnly(); console.log("NFC tag has been made permanently read-only after writing to it."); } catch (error) { console.log(`Operation failed: ${error}`); }
由于 makeReadOnly()
在 Chrome 100 或更高版本中适用于 Android,请通过以下方式检查是否支持此功能:
if ("NDEFReader" in window && "makeReadOnly" in NDEFReader.prototype) { // makeReadOnly() is supported. }
安全与权限
Chrome 团队在设计和实现 Web NFC 时,遵循了控制对强大的 Web 平台功能的访问权限中定义的核心原则,包括用户控制、透明度和人体工程学。
由于 NFC 扩大了恶意网站可能获取的信息范围,因此我们限制了 NFC 的可用性,以最大限度地提高用户对 NFC 使用情况的认知度和控制力。

Web NFC 仅适用于顶级框架和安全浏览上下文(仅限 HTTPS)。来源必须先在处理用户手势(例如点击按钮)时请求 "nfc"
权限。如果之前未授予访问权限,NDEFReader
、scan()
、write()
和 makeReadOnly()
方法会触发用户提示。
document.querySelector("#scanButton").onclick = async () => { const ndef = new NDEFReader(); // Prompt user to allow website to interact with NFC devices. await ndef.scan(); ndef.onreading = event => { // TODO: Handle incoming NDEF messages. }; };
用户发起的权限提示与将设备移至目标 NFC 标记上这一现实世界中的物理移动相结合,与在其他文件和设备访问 API 中找到的选择器模式类似。
如需执行扫描或写入操作,当用户使用设备触碰 NFC 标签时,网页必须处于可见状态。浏览器使用触感反馈来指示点按操作。如果显示屏处于关闭状态或设备处于锁定状态,则对 NFC 无线装置的访问会被阻止。对于不可见的网页,接收和推送 NFC 内容的操作会被暂停,并在网页再次变为可见时恢复。
借助 Page Visibility API,可以跟踪文档可见性何时发生变化。
document.onvisibilitychange = event => { if (document.hidden) { // All NFC operations are automatically suspended when document is hidden. } else { // All NFC operations are resumed, if needed. } };
食谱集
以下是一些代码示例,可帮助您快速入门。
检查权限
借助 Permissions API,您可以检查是否已授予 "nfc"
权限。此示例展示了如何扫描 NFC 标记(如果之前已授予访问权限,则无需用户互动;否则,显示一个按钮)。请注意,写入 NFC 标签的机制与此相同,因为它们在底层使用相同的权限。
const ndef = new NDEFReader(); async function startScanning() { await ndef.scan(); ndef.onreading = event => { /* handle NDEF messages */ }; } const nfcPermissionStatus = await navigator.permissions.query({ name: "nfc" }); if (nfcPermissionStatus.state === "granted") { // NFC access was previously granted, so we can start NFC scanning now. startScanning(); } else { // Show a "scan" button. document.querySelector("#scanButton").style.display = "block"; document.querySelector("#scanButton").onclick = event => { // Prompt user to allow UA to send and receive info when they tap NFC devices. startScanning(); }; }
中止 NFC 操作
使用 AbortController
原语可以轻松中止 NFC 操作。以下示例展示了如何通过 NDEFReader scan()
、makeReadOnly()
、write()
方法的选项传递 AbortController
的 signal
,并同时中止两项 NFC 操作。
const abortController = new AbortController(); abortController.signal.onabort = event => { // All NFC operations have been aborted. }; const ndef = new NDEFReader(); await ndef.scan({ signal: abortController.signal }); await ndef.write("Hello world", { signal: abortController.signal }); await ndef.makeReadOnly({ signal: abortController.signal }); document.querySelector("#abortButton").onclick = event => { abortController.abort(); };
写后读
将 write()
与 AbortController
原语结合使用,然后使用 scan()
,可以在向 NFC 标签写入消息后读取该标签。以下示例展示了如何将文本消息写入 NFC 标记,以及如何读取 NFC 标记中的新消息。3 秒后停止扫描。
// Waiting for user to tap NFC tag to write to it... const ndef = new NDEFReader(); await ndef.write("Hello world"); // Success! Message has been written. // Now scanning for 3 seconds... const abortController = new AbortController(); await ndef.scan({ signal: abortController.signal }); const message = await new Promise((resolve) => { ndef.onreading = (event) => resolve(event.message); }); // Success! Message has been read. await new Promise((r) => setTimeout(r, 3000)); abortController.abort(); // Scanning is now stopped.
读取和写入文本记录
文本记录 data
可使用通过记录 encoding
属性实例化的 TextDecoder
进行解码。请注意,文本记录的语言可通过其 lang
属性获取。
function readTextRecord(record) { console.assert(record.recordType === "text"); const textDecoder = new TextDecoder(record.encoding); console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`); }
如需写入简单的文本记录,请将字符串传递给 NDEFReader write()
方法。
const ndef = new NDEFReader(); await ndef.write("Hello World");
文本记录默认采用 UTF-8 编码,并假定使用当前文档的语言,但可以使用完整语法来创建自定义 NDEF 记录,从而指定这两个属性(encoding
和 lang
)。
function a2utf16(string) { let result = new Uint16Array(string.length); for (let i = 0; i < string.length; i++) { result[i] = string.codePointAt(i); } return result; } const textRecord = { recordType: "text", lang: "fr", encoding: "utf-16", data: a2utf16("Bonjour, François !") }; const ndef = new NDEFReader(); await ndef.write({ records: [textRecord] });
读取和写入网址记录
使用 TextDecoder
对记录的 data
进行解码。
function readUrlRecord(record) { console.assert(record.recordType === "url"); const textDecoder = new TextDecoder(); console.log(`URL: ${textDecoder.decode(record.data)}`); }
如需写入网址记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的网址记录定义为一个对象,其中 recordType
键设置为 "url"
,data
键设置为网址字符串。
const urlRecord = { recordType: "url", data:"https://w3c.github.io/web-nfc/" }; const ndef = new NDEFReader(); await ndef.write({ records: [urlRecord] });
读取和写入 MIME 类型记录
MIME 类型记录的 mediaType
属性表示 NDEF 记录载荷的 MIME 类型,以便 data
可以正确解码。例如,使用 JSON.parse
解码 JSON 文本,并使用 Image 元素解码图片数据。
function readMimeRecord(record) { console.assert(record.recordType === "mime"); if (record.mediaType === "application/json") { const textDecoder = new TextDecoder(); console.log(`JSON: ${JSON.parse(decoder.decode(record.data))}`); } else if (record.mediaType.startsWith('image/')) { const blob = new Blob([record.data], { type: record.mediaType }); const img = new Image(); img.src = URL.createObjectURL(blob); document.body.appendChild(img); } else { // TODO: Handle other MIME types. } }
如需写入 MIME 类型记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的 MIME 类型记录定义为一个对象,其中 recordType
键设置为 "mime"
,mediaType
键设置为内容的实际 MIME 类型,data
键设置为一个对象,该对象可以是 ArrayBuffer
,也可以提供对 ArrayBuffer
的视图(例如 Uint8Array
、DataView
)。
const encoder = new TextEncoder(); const data = { firstname: "François", lastname: "Beaufort" }; const jsonRecord = { recordType: "mime", mediaType: "application/json", data: encoder.encode(JSON.stringify(data)) }; const imageRecord = { recordType: "mime", mediaType: "image/png", data: await (await fetch("icon1.png")).arrayBuffer() }; const ndef = new NDEFReader(); await ndef.write({ records: [jsonRecord, imageRecord] });
读取和写入绝对网址记录
绝对网址记录 data
可通过简单的 TextDecoder
进行解码。
function readAbsoluteUrlRecord(record) { console.assert(record.recordType === "absolute-url"); const textDecoder = new TextDecoder(); console.log(`Absolute URL: ${textDecoder.decode(record.data)}`); }
如需写入绝对网址记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的绝对网址记录定义为一个对象,其中 recordType
键设置为 "absolute-url"
,data
键设置为网址字符串。
const absoluteUrlRecord = { recordType: "absolute-url", data:"https://w3c.github.io/web-nfc/" }; const ndef = new NDEFReader(); await ndef.write({ records: [absoluteUrlRecord] });
读取和写入智能海报记录
智能海报记录(用于杂志广告、传单、广告牌等)将某些 Web 内容描述为 NDEF 记录,该记录包含 NDEF 消息作为其载荷。调用 record.toRecords()
将 data
转换为智能海报记录中包含的记录列表。它应包含网址记录、标题的文本记录、图片的 MIME 类型记录,以及一些自定义本地类型记录,例如 ":t"
、":act"
和 ":s"
,分别用于智能海报记录的类型、操作和大小。
本地类型记录仅在包含 NDEF 记录的本地上下文中具有唯一性。如果类型在包含记录的本地上下文之外的含义无关紧要,并且存储空间使用量是硬性限制,则可以使用这些类型。在 Web NFC 中,本地类型记录名称始终以 :
开头(例如 ":t"
、":s"
、":act"
)。这是为了区分文本记录和本地类型文本记录。
function readSmartPosterRecord(smartPosterRecord) { console.assert(record.recordType === "smart-poster"); let action, text, url; for (const record of smartPosterRecord.toRecords()) { if (record.recordType == "text") { const decoder = new TextDecoder(record.encoding); text = decoder.decode(record.data); } else if (record.recordType == "url") { const decoder = new TextDecoder(); url = decoder.decode(record.data); } else if (record.recordType == ":act") { action = record.data.getUint8(0); } else { // TODO: Handle other type of records such as `:t`, `:s`. } } switch (action) { case 0: // Do the action break; case 1: // Save for later break; case 2: // Open for editing break; } }
如需写入智能海报记录,请将 NDEF 消息传递给 NDEFReader write()
方法。NDEF 消息中包含的智能海报记录定义为一个对象,其中 recordType
键设置为 "smart-poster"
,data
键设置为一个对象,该对象(再次)表示智能海报记录中包含的 NDEF 消息。
const encoder = new TextEncoder(); const smartPosterRecord = { recordType: "smart-poster", data: { records: [ { recordType: "url", // URL record for smart poster content data: "https://my.org/content/19911" }, { recordType: "text", // title record for smart poster content data: "Funny dance" }, { recordType: ":t", // type record, a local type to smart poster data: encoder.encode("image/gif") // MIME type of smart poster content }, { recordType: ":s", // size record, a local type to smart poster data: new Uint32Array([4096]) // byte size of smart poster content }, { recordType: ":act", // action record, a local type to smart poster // do the action, in this case open in the browser data: new Uint8Array([0]) }, { recordType: "mime", // icon record, a MIME type record mediaType: "image/png", data: await (await fetch("icon1.png")).arrayBuffer() }, { recordType: "mime", // another icon record mediaType: "image/jpg", data: await (await fetch("icon2.jpg")).arrayBuffer() } ] } }; const ndef = new NDEFReader(); await ndef.write({ records: [smartPosterRecord] });
读取和写入外部类型记录
如需创建应用定义的记录,请使用外部类型记录。这些可能包含一个 NDEF 消息作为可通过 toRecords()
访问的载荷。其名称包含签发组织的域名、一个英文冒号和至少一个字符的类型名称,例如 "example.com:foo"
。
function readExternalTypeRecord(externalTypeRecord) { for (const record of externalTypeRecord.toRecords()) { if (record.recordType == "text") { const decoder = new TextDecoder(record.encoding); console.log(`Text: ${textDecoder.decode(record.data)} (${record.lang})`); } else if (record.recordType == "url") { const decoder = new TextDecoder(); console.log(`URL: ${decoder.decode(record.data)}`); } else { // TODO: Handle other type of records. } } }
如需写入外部类型记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的外部类型记录定义为一个对象,其中 recordType
键设置为外部类型的名称,data
键设置为表示外部类型记录中包含的 NDEF 消息的对象。请注意,data
键也可以是 ArrayBuffer
,或者提供对 ArrayBuffer
的视图(例如 Uint8Array
、DataView
)。
const externalTypeRecord = { recordType: "example.game:a", data: { records: [ { recordType: "url", data: "https://example.game/42" }, { recordType: "text", data: "Game context given here" }, { recordType: "mime", mediaType: "image/png", data: await (await fetch("image.png")).arrayBuffer() } ] } }; const ndef = new NDEFReader(); ndef.write({ records: [externalTypeRecord] });
读取和写入空记录
空记录没有载荷。
如需写入空记录,请将 NDEF 消息字典传递给 NDEFReader write()
方法。NDEF 消息中包含的空记录定义为 recordType
键设置为 "empty"
的对象。
const emptyRecord = { recordType: "empty" }; const ndef = new NDEFReader(); await ndef.write({ records: [emptyRecord] });
浏览器支持
Web NFC 在 Chrome 89 中适用于 Android。
开发者提示
以下是我希望自己在刚开始使用 Web NFC 时就了解的一些事项:
- 在 Web NFC 运行之前,Android 会在操作系统级处理 NFC 标签。
- 您可以在 material.io 上找到 NFC 图标。
- 使用 NDEF 记录
id
可在需要时轻松识别记录。 - 支持 NDEF 的未格式化 NFC 标签包含一条空类型的记录。
- 编写 Android 应用记录非常简单,如下所示。
const encoder = new TextEncoder(); const aarRecord = { recordType: "android.com:pkg", data: encoder.encode("com.example.myapp") }; const ndef = new NDEFReader(); await ndef.write({ records: [aarRecord] });
演示
试用官方示例,并查看一些精彩的 Web NFC 演示:
反馈
Web NFC 社区群组和 Chrome 团队很想了解您对 Web NFC 的想法和体验。
介绍 API 设计
API 是否存在未按预期运行的情况?或者,是否有缺少的方法或属性需要您来实现自己的想法?
在 Web NFC GitHub 代码库中提交规范问题,或在现有问题中添加您的想法。
报告实现方面的问题
您是否发现 Chrome 的实现存在 bug?还是实现与规范不同?
请访问 https://new.crbug.com 提交 bug。请务必尽可能详细地说明问题,提供重现 bug 的简单说明,并将组件设置为 Blink>NFC
。
显示支持
您是否计划使用 Web NFC?您的公开支持有助于 Chrome 团队确定功能优先级,并向其他浏览器供应商展示支持这些功能的重要性。
发送一条推文给 @ChromiumDev,使用 ##WebNFC
主题标签,告诉我们您在何处以及如何使用它。
实用链接
- 规格
- Web NFC 演示
- 跟踪 bug
- ChromeStatus.com 条目
- Blink 组件:
Blink>NFC
致谢
非常感谢 Intel 的员工实现 Web NFC。Google Chrome 依赖于一个由提交者组成的社区,他们共同努力推进 Chromium 项目。并非所有 Chromium 提交者都是 Google 员工,这些贡献者值得特别表彰!