现在可以读取和写入 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 员工,这些贡献者值得特别表彰!