通过 Chrome(Android 版)与 NFC 设备互动

现在可以读取和写入 NFC 标签。

François Beaufort
François Beaufort

什么是 Web NFC?

NFC 的全称是近距离无线通信 (NFC),是一种近距离无线技术。 在 13.56 MHz 运行,实现距离相距的设备之间的通信 小于 10 cm,传输速率最高为 424 kbit/s。

Web NFC 使网站能够对 NFC 标签进行读取和写入 靠近用户设备(通常为 5-10 厘米、2-4 英寸)。 当前范围仅限于 NFC 数据交换格式 (NDEF),一种轻量级 适用于各种不同代码格式的二进制消息格式。

<ph type="x-smartling-placeholder">
</ph> 为手机接通 NFC 标签以交换数据
NFC 操作示意图

建议的用例

Web NFC 仅限于 NDEF,因为 这样写 NDEF 数据就更容易量化低层级 I/O 操作(例如 ISO-DEP、NFC-A/B、NFC-F)、点对点通信模式和基于主机的卡 不支持模拟 (HCE)。

可以使用 Web NFC 的网站的示例包括:

  • 博物馆和美术馆可以展示有关展览的更多信息 当用户将其设备轻触展览附近的 NFC 卡时触发。
  • 广告资源管理网站可以从 更新有关其内容的信息。
  • 会议网站可利用该设备在活动期间扫描 NFC 标记,并确保 它们被锁定以防止进一步更改上面写入的信息。
  • 网站可以使用它来分享设备或服务所需的初始密钥 并在运维套件中部署配置数据 模式。
。 <ph type="x-smartling-placeholder">
</ph> 手机正在扫描多个 NFC 标签
NFC 库存管理图解

当前状态

步骤 状态
1. 创建铺垫消息 完成
2. 创建规范的初始草稿 完成
3. 收集反馈和对设计进行迭代 完成
4. 源试用 完成
5. 投放 完成

使用网络 NFC

功能检测

针对硬件的功能检测与您所习惯的检测不同。 如果存在 NDEFReader,则表示浏览器支持 Web NFC, 而不是是否存在所需的硬件。特别是,如果硬件 则某些调用返回的 promise 将拒绝。我将提供 当我描述NDEFReader时,会非常有用。

if ('NDEFReader' in window) { /* Scan and write NFC tags */ }

术语

NFC 标签是一种被动 NFC 设备,也就是 当有活跃的 NFC 设备(如手机)在附近时感应到感应。NFC 标签 有多种形式和形式,如贴纸、信用卡、手臂手腕 。

<ph type="x-smartling-placeholder">
</ph> 透明 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 类型、绝对网址、外部类型、未知和本地 类型。

<ph type="x-smartling-placeholder">
</ph> NDEF 消息示意图
NDEF 消息示意图

扫描 NFC 标签

如需扫描 NFC 标签,请先实例化新的 NDEFReader 对象。正在呼叫scan() 返回一个 promise。如果之前没有访问权限,系统可能会提示用户 已授予。如果满足以下所有条件,promise 将解析:

  • 只有在响应用户手势(例如触摸手势或 点击鼠标。
  • 用户已允许网站与 NFC 设备互动。
  • 用户的手机支持 NFC。
  • 用户已在手机上启用 NFC。

一旦解析了 promise,传入的 NDEF 消息就可以通过 通过事件监听器订阅 reading 事件。您还应订阅 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 消息已“准备”并承诺 满足以下所有条件时,解析成功:

  • 只有在响应用户手势(例如触摸手势或 点击鼠标。
  • 用户已允许网站与 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 方法。在这种情况下,如果 NDEF 消息 已存储在 NFC 标签中。

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() 适用于 Android 设备的 Chrome 100 或更高版本,请查看 如果以下产品支持此功能:

if ("NDEFReader" in window && "makeReadOnly" in NDEFReader.prototype) {
  // makeReadOnly() is supported.
}

安全与权限

Chrome 团队按照核心原则设计和实现了 Web NFC 详见控制对强大的 Web 平台的访问权限 功能,包括用户控制、透明度和人体工程学。

由于 NFC 扩展了恶意信息可能获取的信息域 限制 NFC 的可用性,以最大限度地提高用户的认知度和 对 NFC 的使用进行控制。

<ph type="x-smartling-placeholder">
</ph> 网站上的 Web NFC 提示的屏幕截图
Web 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 中找到的 JSON 文件。

要执行扫描或写入,网页必须在用户触摸时可见 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 操作。以下示例展示了如何将 signalAbortController通过 NDEFReader 的选项scan()makeReadOnly()write() 方法并同时中止这两项 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()scan()AbortController 基元支持在向 NFC 标签写入消息后对其进行读取。 以下示例展示了如何将短信写入 NFC 标签并读取 NFC 标记中收到新消息。它会在三秒后停止扫描。

// 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 可通过使用TextDecoder 记录 encoding 属性。请注意,文本记录的语言为 通过其 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 编码,并采用当前文档的语言, 这两个属性(encodinglang)都可以使用完整语法进行指定 (用于创建自定义 NDEF 记录)。

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 记录载荷,以便正确解码 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 的视图(例如, Uint8ArrayDataView)。

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 键设置为 一个对象,该对象(再次)表示包含在 智能海报记录。

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] });

读取和写入外部类型记录

如需创建应用定义的记录,请使用外部类型记录。这些问题 包含可通过 toRecords() 访问的 NDEF 消息作为载荷。他们的 name 包含签发机构的域名、冒号和类型 长度至少为一个字符的名称,例如 "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 的视图(例如, Uint8ArrayDataView)。

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] });

浏览器支持

Android 设备(Chrome 89)支持 Web NFC。

开发提示

下面列出了当我开始使用 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 演示:

。 <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">
Chrome 开发者峰会 2019 中的 Web NFC 卡演示

反馈

网络 NFC 社区小组和 Chrome 团队非常希望了解您对 Web NFC 的想法和体验。

向我们介绍 API 设计

API 是否存在无法按预期运行的地方?或者,在那里 缺少实现想法所需的方法或属性?

Web NFC GitHub 代码库中提交规范问题,或将您的想法添加到 一个现有问题。

报告实现存在的问题

您在 Chrome 的实现过程中是否发现了错误?还是 与规范不同?

访问 https://new.crbug.com 提交 bug。务必尽可能多添加一些 提供重现错误的简单说明 组件设置为 Blink>NFCGlitch 非常适用于以下情况: 轻松快速的重现问题

表示支持

您打算使用 Web NFC 吗?您的公开支持对 Chrome 团队有所帮助 向其他浏览器供应商展示重要性 支持他们。

使用 # 标签向 @ChromiumDev 发送推文 #WebNFC 并告诉我们您使用它的地点和方式。

实用链接

致谢

非常感谢 Intel 员工实现 Web NFC。Google Chrome 浏览器 需要一个由提交者组成的社区,他们共同努力,将 Chromium 。并非每位 Chromium 提交者都是 Google 员工, 值得特别表彰的贡献者!