Chrome для Android теперь может читать и записывать NFC-метки.
Что такое Web NFC?
NFC (Near Field Communications) — это технология беспроводной связи ближнего действия на частоте 13,56 МГц, которая обеспечивает передачу данных между устройствами на расстоянии до 10 см со скоростью до 424 кбит/с.
Web NFC дает сайтам возможность читать и записывать NFC-метки, находящиеся в непосредственной близости от устройства пользователя (обычно это 5-10 см). Пока что работает только NDEF (NFC Data Exchange Format) — упрощенный формат обмена двоичными сообщениями для меток различных форматов.
  Предлагаемые варианты использования
В Web NFC используется только NDEF, поскольку свойства системы безопасности для чтения и записи данных NDEF легче измерить количественно. Низкоуровневые операции ввода-вывода (например, ISO-DEP, NFC-A/B, NFC-F), режим одноранговой связи и эмуляция карт на устройстве пользователя (HCE) не поддерживаются.
Примеры сайтов, которые могут использовать Web NFC: - Музеи и художественные галереи: отображение дополнительных сведений об экспонате, когда пользователь касается его NFC-карты телефоном. - Сайты инвентарного учета: чтение и запись данных в NFC-метку на контейнере для обновления сведений о его содержимом. - Сайты конференций: сканирование NFC-бейджиков на мероприятии. - Эту функцию можно использовать для обмена исходными секретными ключами, необходимыми для подготовки устройств или сервисов, а также для развертывания данных конфигурации в рабочем режиме.
  Текущее состояние
Использование Web NFC
Обнаружение функции
Обнаружение функций для оборудования отличается от привычного.
Наличие NDEFReader сообщает, что браузер поддерживает Web NFC, но не говорит
о наличии необходимого оборудования. В частности, если оборудования нет,
то промис от определенных вызовов будет отклонен. Подробнее —
ниже, в описании NDEFReader.
if ('NDEFReader' in window) { /* Сканирование и запись NFC-меток */ }
Терминология
NFC-метка — это пассивное NFC-устройство: оно получает питание от магнитной индукции, когда поблизости находится активное устройство (например, телефон). NFC-метки бывают разных форм и видов: наклейки, пластиковые карты, браслеты и т. д.
  Объект NDEFReader — это точка входа в Web NFC, которая предоставляет
функции для подготовки действий чтения и (или) записи, выполняемых при появлении
NDEF-метки поблизости. Аббревиатура NDEF в NDEFReader означает NFC Data Exchange
Format — упрощенный формат обмена двоичным сообщениями по стандарту NFC Forum.
Объект NDEFReader предназначен для действий с сообщениями NDEF от NFC-меток
и для записи сообщений NDEF в NFC-метки в радиусе действия.
NFC-метка с поддержкой NDEF напоминает записку: кто угодно может ее прочитать, а также записать (если запись разрешена). В ней содержится одно сообщение NDEF, в котором находится одна или несколько NDEF-записей. Каждая NDEF-запись — это двоичная структура с полезными данными и сведениями о соответствующем типе. Web NFC поддерживает следующие типы записей стандарта NFC Forum: пустая, текст, URL-адрес, смарт-плакат, MIME-тип, абсолютный URL-адрес, внешний тип, неизвестный и локальный тип.
  Сканирование NFC-меток
Чтобы сканировать NFC-метки, создайте экземпляр объекта NDEFReader. Вызов scan()
возвращает промис. Если ранее доступ не был получен, пользователю может
быть отправлен запрос. Объект Promise будет разрешен, если выполнятся следующие условия:
- Пользователь разрешил веб-сайту использовать NFC-устройства при прикосновении ими к телефону.
 - Телефон поддерживает NFC.
 - Пользователь включил NFC на телефоне.
 
После разрешения промиса можно подписаться чтение входящих
сообщений NDEF через прослушиватель событий. Также следует подписаться
на события readingerror — чтобы знать, когда поблизости
оказываются несовместимые NFC-метки.
const ndef = new NDEFReader();
ndef.scan().then(() => {
  console.log("Сканирование запущено.");
  ndef.onreadingerror = () => {
    console.log("Не удается прочитать данные из NFC-метки. Попробовать другую?");
  };
  ndef.onreading = event => {
    console.log("NDEF-сообщение прочитано.");
  };
}).catch(error => {
  console.log(`Ошибка! Не удалось запустить сканирование. ${error}.`);
});
Если поблизости NFC-метка, срабатывает событие NDEFReadingEvent с
двумя уникальными свойствами:
serialNumber— серийный номер устройства (например, 00-11-22-33-44-55-66) или пустая строка, если его нет;message— NDEF-сообщение, хранящееся в NFC-метке.
Чтобы прочитать содержимое NDEF-сообщения, запустите цикл по message.records
и обработайте члены data согласно их recordType (тип).
Члены data представлены как DataView поскольку это позволяет
обрабатывать случаи с кодировкой UTF-16.
ndef.onreading = event => {
  const message = event.message;
  for (const record of message.records) {
    console.log("Тип записи:  " + record.recordType);
    console.log("MIME-тип:    " + record.mediaType);
    console.log("Идентификатор записи:    " + record.id);
    switch (record.recordType) {
      case "text":
        // TODO. Прочитать текстовую запись согласно данным, языку и кодировке записи.
        break;
      case "url":
        // TODO. Прочитать запись URL согласно данным записи.
        break;
      default:
        // TODO. Обработка других записей согласно данным записи.
    }
  }
};
Запись NFC-меток
Чтобы записывать NFC-метки, создайте экземпляр объекта NDEFReader. Вызов write()
возвращает промис. Если ранее доступ не был получен, пользователю может
быть отправлен запрос. На этом этапе NDEF-сообщение уже «подготовлено». Объект
Promise будет разрешен, если выполнятся следующие условия:
- Пользователь разрешил веб-сайту использовать NFC-устройства при прикосновении ими к телефону.
 - Телефон поддерживает NFC.
 - Пользователь включил NFC на телефоне.
 - Пользователь коснулся NFC-метки и NDEF-сообщение было записано.
 
Чтобы записать в NFC-метку текст, передайте в метод write() строку.
const ndef = new NDEFReader();
ndef.write(
  "Hello World"
).then(() => {
  console.log("Сообщение записано.");
}).catch(error => {
  console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Чтобы записать в NFC-метку URL-адрес, передайте в метод write() словарь,
представляющий NDEF-сообщение. В примере ниже NDEF-сообщение — это словарь с ключом
records, значение которого — массив записей. В этом случае
запись URL, определенная как объект, у которого ключ recordType имеет значение
"url", а data — строка URL-адреса.
const ndef = new NDEFReader();
ndef.write({
  records: [{ recordType: "url", data: "https://w3c.github.io/web-nfc/" }]
}).then(() => {
  console.log("Сообщение записано.");
}).catch(error => {
  console.log(`Ошибка записи. Повторите попытку. ${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("Сообщение записано.");
}).catch(error => {
  console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Если NFC-метка содержит NDEF-сообщение, не предназначенное для перезаписи,
то в параметрах для метода write() нужно задать свойству overwrite
значение false. В этом случае, если в NFC-метке уже сохранено
NDEF-сообщение, возвращенный промис будет отклонен.
const ndef = new NDEFReader();
ndef.write("Круто! Пишем данные в пустую NFC-метку!", { overwrite: false })
.then(() => {
  console.log("Сообщение записано.");
}).catch(_ => {
  console.log(`Ошибка записи. Повторите попытку. ${error}.`);
});
Безопасность и разрешения
Команда Chrome разработала и внедрила Web NFC согласно принципам, определенным в Контроле доступа к функциям веб-платформы с широкими возможностями, включая пользовательский контроль, прозрачность и удобство.
NFC расширяет область, в которой информация может быть доступна вредоносным сайтам, поэтому использование NFC ограничено, с тем чтобы максимально информировать пользователей и дать им контроль над этой функцией.
  Web NFC используется только во фреймах верхнего уровня и контекстах безопасного
просмотра веб-страниц (только HTTPS). Источник при обработке жеста пользователя (например, нажатия кнопки) сначала
запрашивает разрешение "nfc". Если ранее доступ не был получен, методы scan() и write()
объекта NDEFReader отправляют запрос пользователю.
  document.querySelector("#scanButton").onclick = async () => {
    const ndef = new NDEFReader();
    // Запрос пользователю разрешить сайту взаимодействовать с NFC-устройствами.
    await ndef.scan();
    ndef.onreading = event => {
      // TODO. Обработка входящих NDEF-сообщений.
    };
  };
Сочетание инициированного пользователем запроса на разрешение и физического перемещения устройства по целевой NFC-метке отражает паттерн выбирающего субъекта, который можно найти в других API доступа к файлам и устройствам.
Для сканирования (записи) веб-страница должна быть видимой, когда пользователь касается NFC-метки устройством. Касание отражается браузером с помощью тактильной обратной связи. Если дисплей выключен или устройство заблокировано, доступ к NFC-передатчику блокируется. Если веб-страница невидима, получение и отправка содержимого приостанавливается и возобновляется, когда она становится видимой.
Изменение видимости документа можно отслеживать с помощью Page Visibility API.
document.onvisibilitychange = event => {
  if (document.hidden) {
    // Если документ скрыт, все операции NFC автоматически приостанавливаются.
  } else {
    // При необходимости все операции NFC возобновляются.
  }
};
Справочник
Ниже приведено несколько примеров кода.
Проверка разрешения
С помощью Permissions API можно проверить, было ли получено разрешение
"nfc". В примере показано, как сканировать NFC-метки без взаимодействия с пользователем,
если доступ уже есть (и отобразить кнопку, если он еще не был получен). Такой же
механизм действует и для записи, поскольку используется то же
разрешение.
const ndef = new NDEFReader();
async function startScanning() {
  await ndef.scan();
  ndef.onreading = event => {
    /* обработка NDEF-сообщений */
  };
}
const nfcPermissionStatus = await navigator.permissions.query({ name: "nfc" });
if (nfcPermissionStatus.state === "granted") {
  // Доступ NFC уже был предоставлен — можно начинать сканирование.
  startScanning();
} else {
  // Показать кнопку сканирования.
  document.querySelector("#scanButton").style.display = "block";
  document.querySelector("#scanButton").onclick = event => {
    // Запросить у пользователя разрешение на отправку и получение информации при касании NFC-устройств.
    startScanning();
  };
}
Прерывание операции с NFC
С помощью примитива AbortController можно прервать операции
с NFC. В примере ниже показано, как передать signal из AbortController
через параметры методов scan() и write() объекта NDEFReader и
одновременно прервать обе операции с NFC.
const abortController = new AbortController();
abortController.signal.onabort = event => {
  // Все операции с NFC были прерваны.
};
const ndef = new NDEFReader();
await ndef.scan({ signal: abortController.signal });
await ndef.write("Hello world", { signal: abortController.signal });
document.querySelector("#abortButton").onclick = event => {
  abortController.abort();
};
Чтение и создание текстовой записи
Данные data текстовой записи можно декодировать с помощью экземпляра
TextDecoder, созданного со свойством encoding записи. Язык
текстовой записи хранится в свойстве lang.
function readTextRecord(record) {
  console.assert(record.recordType === "text");
  const textDecoder = new TextDecoder(record.encoding);
  console.log(`Текст: ${textDecoder.decode(record.data)} (${record.lang})`);
}
Сделать простую текстовую запись можно, передав в метод write() объекта NDEFReader строку.
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] });
Чтение и создание записи с URL-адресом
Для декодирования данных data записи используйте TextDecoder.
function readUrlRecord(record) {
  console.assert(record.recordType === "url");
  const textDecoder = new TextDecoder();
  console.log(`URL: ${textDecoder.decode(record.data)}`);
}
Создать запись с URL-адресом можно, передав в метод write() объекта NDEFReader
словарь с NDEF-сообщением. Запись с URL-адресом в NDEF-сообщении определяется как объект, у которого
ключ recordType имеет значение "url", а data —
строка URL.
const urlRecord = {
  recordType: "url",
  data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [urlRecord] });
Чтение и создание записи с MIME-типом
Свойство mediaType записи с MIME-типом представляет собой MIME-тип полезной
нагрузки NDEF-записи, что позволяет декодировать data. Например, для декодирования
JSON-текста используется JSON.parse, а для данных изображения — элемент 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. Добавить обработку других MIME-типов.
  }
}
Создать запись с MIME-типом можно, передав в метод write() объекта NDEFReader
словарь с NDEF-сообщением. Запись с MIME-типом в NDEF-сообщении определяется как объект, у которого
ключ recordType имеет значение "mime", mediaType — MIME-тип
контента, а data — либо объект 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] });
Чтение и создание записи с абсолютным URL-адресом
Данные data записи с абсолютным URL-адресом можно декодировать с помощью простого TextDecoder.
function readAbsoluteUrlRecord(record) {
  console.assert(record.recordType === "absolute-url");
  const textDecoder = new TextDecoder();
  console.log(`Абсолютный URL: ${textDecoder.decode(record.data)}`);
}
Создать запись с абсолютным URL-адресом можно, передав в метод write() объекта
NDEFReader словарь с NDEF-сообщением. Запись с абсолютным URL в NDEF-сообщении определяется как объект,
у которого ключ recordType имеет значение "absolute-url", а data —
строка URL.
const absoluteUrlRecord = {
  recordType: "absolute-url",
  data:"https://w3c.github.io/web-nfc/"
};
const ndef = new NDEFReader();
await ndef.write({ records: [absoluteUrlRecord] });
Чтение и создание записи со смарт-плакатом
Запись со смарт-плакатом (реклама журналов, листовки, рекламные щиты и т. д.)
описывает определенный веб-контент как NDEF-запись с NDEF-сообщением
в качестве полезной нагрузки. Чтобы преобразовать data в список записей из смарт-плаката,
нужно вызвать record.toRecords(). В нем должна быть запись URL, текстовая
запись для заголовка, запись с 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. Обработка записей других типов, например `:t`, `:s`.
    }
  }
  switch (action) {
    case 0:
      // Выполнить действие
      break;
    case 1:
      // Сохранить на потом
      break;
    case 2:
      // Открыть для редактирования
      break;
  }
}
Создать запись со смарт-плакатом можно, передав в метод write() объекта NDEFReader
NDEF-сообщение. Запись со смарт-плакатом в NDEF-сообщении определяется как объект, у которого
ключ recordType имеет значение "smart-poster", а data —
объект, представляющий, опять же, NDEF-сообщение, содержащееся в
записи смарт-плаката.
const encoder = new TextEncoder();
const smartPosterRecord = {
  recordType: "smart-poster",
  data: {
    records: [
      {
        recordType: "url", // Запись с URL-адресом для содержимого смарт-плаката
        data: "https://my.org/content/19911"
      },
      {
        recordType: "text", // Запись с заголовком для содержимого смарт-плаката
        data: "Забавный танец"
      },
      {
        recordType: ":t", // Запись типа, локальный тип для смарт-плаката
        data: encoder.encode("image/gif") // MIME-тип смарт-плаката
      },
      {
        recordType: ":s", // Запись размера, локальный тип для смарт-плаката
        data: new Uint32Array([4096]) // Размер в байтах для смарт-плаката
      },
      {
        recordType: ":act", // Запись действия, локальный тип для смарт-плаката
        // Выполнить действие, в этом случае — открыть в браузере
        data: new Uint8Array([0])
      },
      {
        recordType: "mime", // Запись MIME-типа со значком
        mediaType: "image/png",
        data: await (await fetch("icon1.png")).arrayBuffer()
      },
      {
        recordType: "mime", // Еще одна запись со значком
        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(`Текст: ${textDecoder.decode(record.data)} (${record.lang})`);
    } else if (record.recordType == "url") {
      const decoder = new TextDecoder();
      console.log(`URL: ${decoder.decode(record.data)}`);
    } else {
      // TODO. Обработка записей других типов.
    }
  }
}
Создать запись с внешним типом можно, передав в метод write() объекта
NDEFReader словарь с NDEF-сообщением. Запись с внешним типом в NDEF-сообщении определяется
как объект, у которого в ключе recordType содержится имя внешнего
типа, а в data — объект, представляющий NDEF-сообщение, содержащееся в записи
с внешним типом. Ключ data также может являться либо объектом
ArrayBuffer, либо его представлением
(например, Uint8Array, DataView).
const externalTypeRecord = {
  recordType: "example.game:a",
  data: {
    records: [
      {
        recordType: "url",
        data: "https://example.game/42"
      },
      {
        recordType: "text",
        data: "Здесь дается контекст игры"
      },
      {
        recordType: "mime",
        mediaType: "image/png",
        data: await (await fetch("image.png")).arrayBuffer()
      }
    ]
  }
};
const ndef = new NDEFReader();
ndef.write({ records: [externalTypeRecord] });
Чтение и создание пустой записи
У пустой записи нет полезной нагрузки.
Создать пустую запись можно, передав в метод write() объекта NDEFReader
словарь с NDEF-сообщением. Пустая запись в NDEF-сообщении определяется как объект, у которого
ключ recordType имеет значение "empty".
const emptyRecord = {
  recordType: "empty"
};
const ndef = new NDEFReader();
await ndef.write({ records: [emptyRecord] });
Поддержка в браузере
Web NFC поддерживается на Android в Chrome 89.
Советы разработчикам
Ниже — то, что было бы неплохо знать до начала работы с Web NFC:
- Android обрабатывает NFC-метки на уровне ОС до того, как начинает действовать Web NFC.
 - Значок NFC можно найти на сайте material.io.
 - Различать NDEF-записи легче всего, используя их 
id. - NFC-метка без формата с поддержкой NDEF содержит одну запись пустого типа.
 - Ссылка на приложение 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: - Карточки. - Список продуктов. - Intel RSP Sensor NFC. - Мультимедийная записка.
Отзывы
Группа сообщества Web NFC и команда Chrome с радостью выслушает ваше мнение об Web NFC и опыте использования этой функции.
Ваше мнение о дизайне API
Что-то в API не работает должным образом? Или, может, отсутствуют методы или свойства, необходимые для реализации вашей идеи?
Отправьте заявку о проблеме со спецификацией (Spec issue) в GitHub-репозиторий Web NFC или прокомментируйте существующую.
Сообщите о проблеме с реализацией
Нашли ошибку в реализации функции в браузере Chrome? Реализация отличается от спецификации?
Сообщите об ошибке на странице https://new.crbug.com. Опишите
проблему как можно подробнее, дайте простые инструкции по ее воспроизведению
и для Components укажите Blink>NFC. Для демонстрации этапов воспроизведения
ошибки удобно использовать Glitch.
Окажите поддержку
Планируете использовать Web NFC? Ваша публичная поддержка помогает команде Chrome определять приоритет функций и показывает другим поставщикам браузеров, насколько важно их поддерживать.
Упомяните в твите @ChromiumDev, поставьте хештег
#WebNFC
и расскажите, как вы используете эту функцию.
Полезные ссылки
- Спецификация.
 - Демонстрационный пример Web NFC | Исходный код демопримера Web NFC.
 - Отслеживание ошибок.
 - Запись на ChromeStatus.com.
 - Компонент Blink: 
Blink>NFC. 
Благодарности
Большое спасибо ребятам из Intel за реализацию Web NFC. Google Chrome опирается на сообщество разработчиков, вместе продвигающих проект Chromium вперед. Не все авторы кода Chromium — сотрудники Google, поэтому они заслуживают особой благодарности!