Телефон поднесли к бумажной наклейке на стойке, и на экране мгновенно открылась карточка товара с остатками на складе. Никаких приложений, никакого сканирования камерой, никакого ожидания. Это не футуристический сценарий из презентаций, а рабочий сценарий Web NFC, экспериментального браузерного интерфейса, который умеет читать и записывать метки ближнего поля прямо из веб-страницы. Разбираемся, как он устроен, где проходит граница возможностей и почему технология до сих пор не стала массовой.
Web NFC API работает только с метками формата NDEF и требует защищённого соединения
Технология NFC сама по себе известна давно, но браузер получил к ней доступ только в узком сценарии. Интерфейс NDEFReader из состава Web NFC API позволяет читать данные с совместимых устройств NFC и записывать их, когда метка попадает в поле магнитной индукции считывателя. Ключевое ограничение зашито в названии формата: работает только NDEF, NFC Data Exchange Format.
Для разработчика это означает, что низкоуровневые операции с секторами и блоками памяти недоступны. Браузер не даст работать с меткой как с произвольным хранилищем байтов, он оперирует сообщениями NDEF, состоящими из записей. Каждая запись имеет тип, определяющий смысл данных, и именно вокруг типов строится вся логика приложения.
Второе жёсткое условие касается контекста. Web NFC работает только в защищённом контексте, то есть страница обязана открываться по HTTPS. Это не прихоть разработчиков спецификации, а следствие чувствительности сценария: чтение метки по сути означает физическое присутствие пользователя рядом с объектом, и подмена такого канала открывала бы простор для подлога.
Третье ограничение самое болезненное для планирования проектов. Технология помечена как экспериментальная, её нет в Baseline, потому что часть массовых браузеров её не поддерживает. Практический вывод прост: Web NFC не подходит для сервиса, который обязан работать у всех, но отлично ложится на внутренние инструменты, промо-стенды, складские терминалы и корпоративные приложения с известным парком устройств.
Сканирование запускается методом scan с ожиданием разрешения пользователя
Работа с чтением начинается с создания объекта NDEFReader и вызова метода scan(). Метод активирует считывающее устройство и возвращает промис, который завершается успешно, когда операция чтения поставлена в очередь, либо отклоняется при аппаратной ошибке или отказе в разрешении. Если разрешение nfc ещё не выдавалось, браузер покажет пользователю запрос, и без его согласия сценарий не пойдёт дальше.
Отказ приходит в виде DOMException с говорящими именами. AbortError означает, что сканирование прервали через переданный AbortSignal. InvalidStateError сообщает о попытке запустить второе сканирование при уже идущем первом. NotAllowedError фиксирует запрет на операцию. NotSupportedError сигнализирует, что подходящего адаптера NFC нет или соединение с ним не устанавливается.
Так выглядит минимальный запуск чтения:
const ndef = new NDEFReader();
ndef.scan()
.then(() => {
ndef.onreadingerror = (event) => {
console.log("Метку видно, но прочитать не удалось");
};
ndef.onreading = (event) => {
console.log("Сообщение прочитано");
};
})
.catch((error) => {
console.log(`Сканирование не запустилось: ${error}`);
});
Здесь важны две разные точки отказа. Событие readingerror возникает, когда метка оказалась рядом со считывателем, но прочитать её не получилось. Такое случается с повреждёнными или несовместимыми метками, и приложение обязано предложить пользователю заменить предмет, а не молча зависнуть. Событие reading приходит, когда данные действительно получены, и именно в его обработчике начинается вся прикладная логика.
Отдельная тонкость связана с таймаутом. Промис scan() разрешается сразу после постановки чтения в очередь, а не в момент, когда пользователь приложил метку. Если нужно ограничить ожидание, например потребовать касание в течение пятнадцати секунд, придётся строить собственный AbortController и обрывать операцию по таймеру. Спецификация подсказывает этот приём через сигнал отмены, он же пригодится при верстке экранов ожидания.
Сообщение состоит из записей с типами text, url, mime и smart-poster
Прочитанное сообщение приходит в обработчик события как объект NDEFMessage, у которого есть одно доступное свойство records. Это список записей NDEFRecord. Сама запись открывает разработчику набор полей только для чтения: recordType, mediaType, id, data, encoding, lang.
Тип записи определяет, как трактовать полезную нагрузку. Стандартные имена включают empty, text, url, smart-poster, absolute-url, mime и unknown. Кроме стандартных, допустимы внешние имена, которые собираются из доменного имени и собственного названия типа, разделённых двоеточием. Скажем, компания вправе завести свой тип вроде inventory.example.com:batch, чтобы передавать номер партии.
Смысл полей легко разобрать на примере записи с типом mime. Здесь mediaType вернёт конкретный MIME-тип, скажем application/json, а поле data отдаст сырые байты в виде DataView. Для типов, содержащих текст, заполняются encoding и lang, то есть кодировка и языковая метка по BCP 47. Поле id хранит идентификатор записи, причём его уникальность обеспечивает только тот, кто запись создал, браузер это не проверяет.
Разбор сырых байтов удобно вести через декодер:
ndef.onreading = (event) => {
for (const record of event.message.records) {
if (record.recordType === "text") {
const decoder = new TextDecoder(record.encoding || "utf-8");
console.log("Текст:", decoder.decode(record.data));
}
if (record.recordType === "mime") {
console.log("Тип данных:", record.mediaType);
}
if (record.recordType === "url") {
const decoder = new TextDecoder();
console.log("Адрес:", decoder.decode(record.data));
}
}
};
Особая история со вложенными записями. Тип smart-poster по замыслу содержит внутри себя другие записи, поэтому у NDEFRecord есть метод toRecords(). Он разворачивает содержимое data в последовательность вложенных записей. Тот же механизм применяется к внешним типам, где полезная нагрузка может нести составную структуру. Приложение, которое умеет только читать плоский список, на умном постере споткнётся и покажет пустоту.
Запись на метку выполняется методом write с проверкой через событие чтения
Метод write() пытается записать сообщение NDEF на метку и возвращает промис, который завершается успешно после записи или отклоняется при ошибке оборудования либо разрешений. Сообщение можно передать разными способами: строкой, ArrayBuffer, типизированным массивом, DataView или массивом записей.
Второй аргумент задаёт параметры операции. Флаг overwrite решает, затирать ли уже существующие записи. Если поставить его в false при наличии данных на метке, операция завершится ошибкой NotAllowedError. Объект AbortSignal позволяет отменить запись. Ошибка NotSupportedError приходит, когда адаптера нет или он не умеет отправлять сообщения. NotReadableError означает, что браузеру запрещён доступ к адаптеру на уровне пользовательских настроек. NetworkError фиксирует обрыв передачи, например когда метку убрали от считывателя раньше времени.
Запись лучше запускать не отдельным действием, а в ответ на событие чтения. Спецификация прямо рекомендует этот приём: сначала метка прочитывается, приложение убеждается, что перед ним тот самый объект, и только потом пишет в него. Иначе есть риск случайно перезаписать чужую метку, которая оказалась ближе.
Пример аккуратной связки чтения и записи:
const ndef = new NDEFReader();
let ignoreRead = false;
ndef.onreading = (event) => {
if (ignoreRead) {
return;
}
console.log("Метка прочитана вне процесса записи");
};
function write(data) {
ignoreRead = true;
return new Promise((resolve, reject) => {
ndef.addEventListener(
"reading",
(event) => {
ndef
.write(data)
.then(resolve, reject)
.finally(() => (ignoreRead = false));
},
{ once: true },
);
});
}
await ndef.scan();
try {
await write("Склад А, место 14");
console.log("Запись выполнена");
} catch (err) {
console.error("Что-то пошло не так", err);
}
Флаг ignoreRead здесь решает конкретную задачу: он не даёт общему обработчику срабатывать на то чтение, которое запущено ради записи. Без него приложение дважды отреагирует на одно касание и, например, дважды отправит запрос на сервер.
Ограничение по объёму данных упирается в саму метку. Обычные наклейки вмещают от нескольких десятков до нескольких сотен байт, и попытка записать длинный текст закончится ошибкой. Практичный приём состоит в том, чтобы писать на метку короткий идентификатор, а остальные сведения держать на сервере и подтягивать по этому ключу.
Web NFC встраивается в реальные сценарии от склада до музейного зала
Узость поддержки не мешает технологии решать конкретные задачи в закрытых условиях. Когда парк устройств известен и заранее определён, экспериментальный статус перестаёт быть препятствием.
Первое направление связано с учётом. Кладовщик прикладывает телефон к метке на полке, браузерное приложение показывает остатки и предлагает списать или добавить позицию. Ничего устанавливать не нужно, достаточно открыть внутренний портал. Метка умеет хранить идентификатор места или партии, а вся тяжёлая логика остаётся на сервере.
Второе направление касается выставочных пространств. Посетитель подносит телефон к табличке у экспоната, и в браузере открывается страница с описанием, аудиодорожкой и связанными материалами. Здесь ценна замена громоздких QR-кодов, которые плохо читаются при слабом освещении и требуют прицеливания камерой.
Третье направление лежит в логистике. Приёмка товара упирается в сверку документов, и метка на коробке позволяет одним касанием подтянуть накладную. Приложение сравнивает записанный на метке код с данными в системе и подсвечивает расхождения.
Четвёртый сценарий встречается в обслуживании оборудования. Метка на щитке или агрегате хранит модель и дату последней проверки, а мастер через браузер видит историю работ и оставляет отметку о выполненной операции.
Во всех перечисленных случаях есть общий признак успеха: пользователь экономит несколько секунд на каждой операции, и за смену эти секунды набегают в часы. Именно на таких задачах узкая технология окупается, тогда как попытка построить на ней массовый сервис для неопределённой аудитории обречена на разочарование.
Безопасность и совместимость определяют границы применимости технологии
Защита в Web NFC устроена так. Доступ к адаптеру невозможен без защищённого контекста. Разрешение запрашивается у пользователя и привязано к источнику страницы. Фоновое чтение исключено, страница должна быть активной. Метка не может незаметно инициировать действие: приложение само решает, что делать с прочитанными данными.
Обратная сторона медали тоже заметна. Метка, лежащая в общедоступном месте, может быть подменена злоумышленником на другую с иным содержимым. Приложение обязано проверять прочитанные данные, а не доверять им слепо, особенно если по метке списываются товары или выдаются права доступа. Простейшая мера защиты состоит в криптографической подписи полезной нагрузки: на метку пишется код, а сервер проверяет его подлинность.
Совместимость остаётся главной практической головной болью. Перед началом проекта стоит проверить поведение в целевых браузерах и предусмотреть понятный отказ. Хорошая проверка выглядит так:
if ("NDEFReader" in window) {
const ndef = new NDEFReader();
await ndef.scan();
} else {
console.log("Чтение меток в этом браузере недоступно");
}
Такой заслон обязателен. Без него пользователь на неподдерживаемом устройстве увидит лишь пустой экран или невнятную ошибку в консоли, а впечатление сложится как о сломанном продукте.
Полезно помнить и о физике. Радиус действия считывателя в телефоне измеряется сантиметрами, обычно от одного до четырёх. Металл рядом с меткой ослабляет поле, поэтому наклейки на металлических поверхностях требуют специального исполнения с экранирующей прокладкой. Метки на бумажной основе боятся перегибов и влаги. Эти бытовые детали чаще срывают внедрение, чем код.
Технология чтения меток из браузера получилась намеренно скромной. Она не пытается заменить нативные приложения и не обещает чудес, зато даёт веб-разработчику прямой канал к физическому миру без установки и без посредников. Когда задача сводится к одному касанию вместо десяти нажатий, эта скромность превращается в преимущество. Дальше всё зависит от того, найдётся ли рядом метка, которую стоит прочитать.