Если человек пользуется автозаполнением в браузере, он ждёт, что сможет быстро заполнять формы на любом сайте, где посчитает нужным. Наладить такой механизм на стороне сайта или веб-приложения несложно, но важно помнить пару вещей — я покажу кейсы, где подходы «в лоб» приводили к непредсказуемым результатам. Чтобы автозаполнение работало эффективно и не нарушало логику, стоит хотя бы примерно представлять, как оно устроено под капотом разных браузеров, которые могут быть у пользователей. Под катом распишу, каким образом движок подставляет данные в формы.

Пароли, карты, личные данные


Есть три основные группы форм, где обычно требуется автозаполнение: личные данные, банковские карты и пароли.

Самая широкая группа — личные данные. К ним относятся имена, адреса, телефоны, дата рождения, пол и всё, что не попадает в группу паролей и банковских карт. У этих данных очень разный формат, поэтому часто браузеры и сайты слабо прорабатывают автозаполнение. В статье я в основном буду говорить про заполнение личных данных.

У банковских карт есть особенности сохранения и подстановки, так как речь идёт про очень чувствительные данные. Допустим, браузеры на основе Chromium, в том числе Яндекс Браузер, запрещают подстановку данных карты на сайтах с небезопасным http-соединением — и требуют https. В целом, обработка данных банковских карт укладывается в общий с личными данными механизм. Я не буду углубляться в особенности автозаполнения банковских карт, но большая часть статьи будет актуальна и для этой группы.

Пароли — тоже чувствительная к безопасности группа данных. Существенная часть всех автозаполнений приходится именно на парольные формы, так что этот сценарий лучше всего проработан браузерами и сайтами. В Chromium механизм сохранения и автозаполнения паролей сильно отличается от аналогичного для личных данных и банковских карт. В этой статье углубляться в парольную специфику мы не станем.

Как это работает?


Чтобы понять, как работает автозаполнение, предположим, что у браузера уже есть сохранённые данные пользователя. К механизму их получения перейдём ниже, а пока рассмотрим вставку значения в поле с момента, когда пользователь нажал на это поле:

  1. Браузер получает сигнал о том, что нужно подготовить подсказки для заполнения поля. Как правило, этот сигнал происходит в момент, когда пользователь устанавливает фокус в поле.
  2. Браузер собирает информацию о поле и для целей автозаполнения его интересуют в первую очередь атрибуты autocomplete, name, placeholder, id и label.
  3. В зависимости от собранной информации, браузер вычисляет подсказку с данными для автозаполнения на основе алгоритма:
    • если указан атрибут autocomplete с указанием типа, то он выдаёт соответствующие данные, например ФИО, адрес или дату рождения;
    • в противном случае браузер пробует вычислить тип на основе значений атрибутов name, placeholder, id и label и возвращает соответствующие данные;
    • если вычислить тип не удаётся, то браузер ищет историческую подсказку, то есть ранее сохранённое значение для поля по значению его атрибута name.
  4. Пользователь выбирает подсказку, и браузер подставляет значения в поля формы на странице.

Интересно, что само по себе заполнение полей на странице не всегда происходит гладко и без багов. Например, номера телефонов доставляют целый ворох проблем. В России номера начинаются на +7, а люди часто используют вместо этого 8. Браузер может хранить номера в обоих форматах. Проблемы начинаются во время вставки:

  • Сайт может запрещать редактировать первую цифру и давать ввести номер, начиная с 9.
  • Сайт может позволить вставить 8, ожидая +7, и жаловаться на неверный формат номера.
  • Сайты могут самовольно форматировать введённый номер и не всегда оказываются готовы к автозаполнению. Они добавляют пробелы, скобочки или дефисы и портят номер телефона так, что пользователю приходится его стирать и вводить вручную.
  • На сайтах можно встретить выпадающие списки, которые тоже не всегда готовы к автозаполнению со стороны браузера и ломаются, потому что ожидают, что поле будет пустым, пока пользователь не выберет значение из списка.

Программируйте системы форматирования, выпадающие списки и формы в целом с учётом того, что данные в поле попадут со стороны браузера, а не только в ходе заполнения пользователем.

Так выглядит автозаполнение от выделения поля до вставки в него значения в общем виде. Самый интересный и сложный этап — вычисление подсказки. Разобьём его на несколько тем:

  • система типов полей,
  • исторические подсказки,
  • атрибут autocomplete.

Система типов полей


Тип поля — характеристика, однозначно описывающая данные, которые это поле ожидает в качестве ввода. Типов полей бывает очень много. За их добавление и поддержку отвечают разработчики браузера. Задача типов — однозначно сопоставить поле и соответствующую ему информацию о пользователе.

Примеры типов полей представлены в таблице ниже, а их полный список можно посмотреть в исходниках Chromium.
Тип Описание Пример
UNKNOWN_TYPE Тип данных не определился -
NAME_FIRST Имя Александр
NAME_MIDDLE Отчество Александрович
NAME_LAST Фамилия Алехин
NAME_FULL ФИО Александр
Александрович
Алехин
EMAIL_ADDRESS Электронная почта test@ya.ru
ADDRESS_HOME_LINE1 Улица и дом Льва Толстого, 16
ADDRESS_HOME_CITY Город Москва
ADDRESS_HOME_STATE Область Московская
ADDRESS_HOME_ZIP Индекс 119021
ADDRESS_HOME_COUNTRY Страна Россия
CREDIT_CARD_NAME_FULL Полное имя владельца
банковской карты
Ivanov Ivan
CREDIT_CARD_NUMBER Номер банковской карты 5555 5555 5555 4444
CREDIT_CARD_VERIFICATION_CODE CVV банковской карты 123
USERNAME Логин User0197
Автозаполнение на основе типов гарантирует, что в поле окажутся данные только конкретного типа. Допустим, в поле адреса не окажется ФИО. В этом и есть его важное преимущество и отличие от исторических подсказок, которые работают без привязки к типам.

Регулярные выражения — важный инструмент в определении типов. С их помощью сопоставляются значения атрибутов полей с существующими типами. Проще всего объяснить логику работы регулярных выражений с помощью примеров.

Примеры успешных сопоставлений полей и типов:

<form>
  <input id="first_name"> <!-- NAME_FIRST -->
  <input name="email"> <!-- EMAIL_ADDRESS -->
  <textarea placeholder="Введите адрес"> <!-- ADDRESS_HOME_LINE1 -->
</form>

В примере успешных сопоставлений браузер видит на одном из полей атрибут id со значением first_name. Регулярные выражения сопоставляют это значение с типом NAME_FIRST и таким образом определяют тип поля. Аналогично определяются типы других полей.

Примеры неудавшихся сопоставлений:

<form>
  <input id="xdia_13"> <!-- UNKNOWN_TYPE -->
  <input name="address_mail"> <!-- UNKNOWN_TYPE -->
  <textarea placeholder="Адрес почты"></textarea> <!-- ADDRESS_HOME_STREET_ADDRESS -->
</form>

Здесь заметно несовершенство подхода с регулярными выражениями. Оно проявляется в атрибутах, по которым невозможно установить тип. Яркий пример — неосмысленные значения полей, как id="xdia_13" в примере выше. Регулярные выражения по-разному настроены для разных языков. Поэтому в полях с, казалось бы, идентичными значениями name="address_mail" и placeholder="Адрес почты" тип определяется по-разному.

Другая проблема состоит в логически неверном определении типа. Такое случается, если атрибуты недостаточно точно говорят о назначении поля. Допустим, сайт может предложить пользователю заполнить фамилию и имя в двух разных полях. При этом поле с именем отмечено атрибутом name=“name”. В этом случае браузер определит тип поля как NAME_FULL, то есть ФИО, а не NAME_FIRST — только имя, и покажет неверную подсказку или неверно сохранит данные.

Другой подобный пример связан с датами. В форме покупки авиабилета часто встречается поле с датой вылета. Браузер может вычислить тип такого поля как дату рождения пользователя, с неприятными последствиями в виде неверной подсказки или сохранения неверной информации.

Как видим, система, основанная на типах, имеет свои минусы:

  • требует дополнительных вычислений;
  • определение типа в ней не всегда возможно и иногда оно работает неверно;
  • лишена гибкости, потому что типы определяют разработчики браузера, и сложно предусмотреть все виды полей.

Разработчики сайтов могут осмысленно заполнять атрибуты name, placeholder, id и label. Это поможет браузеру верно определить тип поля и улучшить опыт взаимодействия с сайтом.

Исторические подсказки


Система типов полей не всегда работает надёжно. Ей могут помешать неосмысленные значения атрибутов или поле может изначально не попадать ни под один известный тип. В этом случае на помощь приходит ещё один механизм, который вносит свою большую лепту в автозаполнение. Мы называем его историческими подсказками.

Суть механизма состоит в том, что браузер пробует найти в своём хранилище данные для заполняемого поля по его атрибуту name. Если такие данные нашлись, то пользователь увидит подсказку. Если нет, то после заполнения и отправки формы значение поля будет сохранено в виде пары: name: value. Далее браузер сможет использовать эту пару для формирования подсказки, в том числе и на других сайтах, если встретит там поле с уже известным именем. Периодически браузер удаляет записи, которые не использовались определённое время.

Важно отметить, что браузер накладывает некоторые условия на сохранение таких полей. Например, у поля не должно быть значения атрибута autocomplete=“off”. Значение поля сохраняться не будет, и браузер не подскажет его пользователю.

Логично ожидать, что autocomplete=“off” выключит автозаполнение и сохранение информации из поля. По факту это выключит только исторические подсказки, но пользователи могут увидеть другие — основанные на типах! Дело в том, что стандарт HTML даёт весьма нечёткое определение поведения для некоторых значений атрибута.

Атрибут autocomplete


С помощью атрибута autocomplete разработчики сайта могут вручную контролировать поведение автозаполнения. Значениями атрибута могут быть “on”, “off” или тип поля, например “street-address” или “username”. Этот инструмент приоритетнее остальных, он прописан в стандарте HTML. Это самый надёжный способ управлять автозаполнением.

Можно сказать, что огромная машинерия браузера по угадыванию типов полей со всеми недостатками, ошибками и проблемами во многом нужна только потому, что сайты зачастую игнорируют autocomplete. В лучшем случае они используют autocomplete=”on”/”off”.

Значение autocomplete=”on” согласно стандарту подразумевает, что браузер волен сохранять и подсказывать значения как угодно:

The «on» keyword indicates that the user agent is allowed to provide the user with autocompletion values, but does not provide any further information about what kind of data the user might be expected to enter.

Намного интереснее значение autocomplete=”off”. Логично ожидать, что так можно выключить все подсказки для поля, но это носит рекомендательный характер. В стандарте написано, что не следует сохранять данные из поля и предлагать ранее сохранённые значения. В случае обязательных требований стандарт использует must/must not.

When an element's autofill field name is «off», the user agent should not remember the control's data, and should not offer past values to the user.

Здесь и ниже под autofill field name подразумевается значение атрибута autocomplete.

Более того, в стандарте можно найти формулировку, согласно которой браузер позволяет пользователю самому изменить значение autocomplete=”off” и сохранить заполненные данные или автозаполнить поле.

A user agent may allow the user to override an element's autofill field name, e.g. to change it from «off» to «on» to allow values to be remembered and prefilled despite the page author's objections, or to always «off», never remembering values.

Другая формулировка вообще говорит, что браузер может рассмотреть возможность менять значение autocomplete.

More specifically, user agents may in particular consider replacing the autofill field name of form controls…

Согласно стандарту мусорные значения типа “none”, “smth” и другие наряду с отсутствием атрибута нужно интерпретировать как поведение по умолчанию, то есть “on”: Processing model, пункт 5. Любопытно, что в этой части Chromium отходит от стандарта и не предлагает подсказки для полей с мусорными значениями. Яндекс Браузер поступает так же. Таким образом, самый надёжный костыль способ выключить автозаполнение с помощью атрибута — задать ему мусорное значение типа “none”, “smth” или любое другое.

Стандарт HTML весьма расплывчато регламентирует работу автозаполнения. Дисклеймер стандарта в части автозаполнения помечен как «This section is non-normative», из-за чего браузеры вынуждены по-своему его интерпретировать. Поэтому поведение атрибута autocomplete со значением “off” только на первый взгляд кажется неожиданным.

Таким образом, Chromium интерпретирует autocomplete=“off” как отключение исключительно исторических подсказок. Браузер изменит значение “off” на “on”, если смог определить тип поля, и покажет подсказку, основанную на типе.

В то же время стандарт весьма чётко определяет поведение атрибута со значением “on” и особенно с конкретным типом. Получается, что лучший способ управлять автозаполнением на странице — указать тот тип для поля, который максимально соответствует его смыслу.

Отправка формы


Дальше поговорим о том, как браузер получает данные для последующей подстановки. Отправка формы — единственный источник данных о пользователе для браузера. Также браузер даёт пользователям возможность самим сохранить имя, адрес, банковскую карту для оплаты покупок и прочие данные в настройках. Но доля данных, сохранённых таким образом, ничтожно мала по сравнению с отправкой.

В самом простом случае браузер обрабатывает событие submit согласно стандартному поведению отправки формы — это сигнал для сохранения данных. Однако далеко не все формы отправляют такое событие. Многие формы используют нестандартные механизмы отправки данных на сервер. А ещё многие сайты предлагают пользователям заполнять данные в формах, где нет тега — назовём их бесформенными формами. Это так же мешает браузеру обработать отправку и сохранить данные (ниже обсудим, почему). Всё это вынуждает разработчиков браузера изобретать дополнительные способы наблюдения отправки формы на сайте.

Выделим несколько сценариев отправки с примерами:

  • С тегом и событием.

    <form>  <!-- +form -->
      <input type="text">
      <input type="text">
      
      <input type="submit"> <!-- +submit -->
    </form>
  • С тегом, но без события.

    <form> <!-- +form -->
      <input type="text">
      <input type="text">
     
      <input type=“button”> <!-- -submit -->
    </form>
  • Без тега, но с событием.

    <div> <!-- -form -->
      <input type="text">
      <input type="text">
      
      <input type="submit"> <!-- +submit -->
    </div>
  • Без тега и без события.

    <div> <!-- -form -->
      <input type="text">
      <input type="text">
      
      <div>Button</div> <!-- -submit -->
    </div>

В интернете встречаются все перечисленные комбинации, поэтому браузеру важно уметь работать с отправкой всевозможных форм.

iOS-специфика

Я занимаюсь автозаполнением в Яндекс Браузере на iOS, поэтому примеры ниже будут про эту платформу. По большей части логика сохранения данных и наблюдения за отправкой форм одинакова для всех платформ. Механизм строится по одному и тому же алгоритму и будет интересен вне зависимости от ваших предпочтений в платформе. Но у iOS есть своя специфика, которую важно объяснить прежде, чем мы углубимся в реализацию наблюдения отправки форм.

WebKit


Особенность разработки браузера на iOS состоит в том, что в работе со страницами приходится полагаться на движок WebKit. Он отвечает за рендеринг страницы, хранит данные HTML-формы, позволяет работать с JavaScript и прочее. Desktop и Android работают со страницей с помощью движка Blink. При желании вы можете изменить его код и добиться нужного поведения. На iOS приходится подстраиваться под API, которое предоставляет WebKit. При этом Apple в своём браузере Safari использует так называемое приватное API (методы движка WebKit), которое запрещает использовать его другим разработчикам. К счастью, есть способы обойти ограничение, но они могут не понравиться Apple.

Например, внутри WebKit есть функция, которая сообщает в нативный код браузера информацию об отправке формы. Выглядит так, что это то, что нужно, чтобы отловить submit, и даже Blink работает по аналогии. Но использовать эту функцию нельзя, так как она спрятана в недрах WebKit и не является открытым API. Это лишь одно из ограничений. По сравнению с Blink их очень много.

Chromium


Для обхода ограничений Chromium я нашёл «изящный» выход. При загрузке страницы браузер добавляет к ней скрипт, в котором подписывается на события JavaScript. С их помощью браузер отлавливает события отправки форм, изменения DOM-дерева, заполняет поля формы и многое другое. Нативный код в iOS взаимодействует со страницей с помощью JavaScript, в отличие от Android и Desktop. Они взаимодействуют со страницей напрямую из нативного кода благодаря движку Blink.

У подхода со скриптом есть свои минусы:

  • скорость выполнения JavaScript ниже нативного кода;
  • асинхронный обмен данными со страницей;
  • дублирование логики, схожей с Blink, и переписывание её на JavaScript;
  • код страницы может делать непонятное и ломать работу автозаполнения браузера, например, переписывать стандартный конструктор строки (случай из реальной жизни).

Скрипт добавляет обработчик на JS-событие submit, и он оповещает нативный код об этом событии. И это единственный способ узнать об отправке формы с личными данными в Chromium для iOS на текущий момент.

Важно отметить, что ситуация с парольными формами обстоит значительно лучше. Для них Chromium обрабатывает поля без тега и применяет эвристики по определению отправки при отсутствии явного события submit. То есть разработчики браузеров пришли к тому, что странно полагаться только на submit.

Итак, Chromium на iOS не умеет анализировать бесформенные формы с личными данными и оставляет их без внимания, потому что они выпадают из стандартного механизма отправки. Это ведёт к тому, что значительная часть введённых данных не сохраняется, и пользователь не увидит в дальнейшем подсказку, если будет заполнять похожую форму.

В парольных формах с наиболее чувствительной информацией Chromium ориентируется на событие click. Это менее говорящее событие, поэтому браузер вынужден делать дополнительные проверки, чтобы убедиться, что произошла отправка. Суть проверки состоит в том, чтобы понять, что нажатие случилось по кнопке, и это единственная на форме кнопка.

Эти элементы считаются кнопками:

<input type=”submit”/>
<button type=”submit”/>
<button/>
<button type=”button”/>

Таким образом, на отправку может указывать нажатие кнопки в том числе без явного JS-события submit.

Бесформенные формы


Отсутствие тега вносит сложность во многие аспекты механизма автозаполнения, например вставку данных, обработку отправки и сохранение данных.

Основная сложность в работе с бесформенными формами заключается в том, что браузер не может чётко определить границы заполняемой формы. Фактически браузеру приходится анализировать всю страницу с целью собрать все поля ввода и найти кнопки, которые могут вызвать отправку. Ограничение на клик по единственной на форме кнопке для таких форм превращается в ограничение на единственную кнопку на странице.

Немного острее проблема проявляется в single-page applications, где отправиться может малая часть полей, например адрес доставки, но браузер вынужден заодно считать отправленными все остальные поля — почту и телефон, которые могут быть заполненными не полностью. На удивление, таких форм очень много, и по нашим данным, около трети отправок на Android- и Desktop-версии Яндекс Браузера приходится на такие формы.

Эвристики


У браузера есть ещё несколько козырей в рукаве для того, чтобы обнаружить отправку формы, в том числе по сценарию с отсутствием тега и события submit. Более того, многие формы достаточно упороты сложны, чтобы браузер не смог определить отправку и в других сценариях с помощью JavaScript, например:

<form>
 <input type=”text”/>
 <button type=”button”/>
 <div role=”button”>Button</div>
</form>

Или:

<input type=”text”/>
<button/>
<button/>

Видно, что эти примеры с точки зрения автозаполнения отличаются лишь наличием тега . С этими формами мы наблюдаем две проблемы, которые не дают поймать отправку:

  • нет события submit,
  • есть несколько кнопок, и мы не можем с уверенностью считать клик отправкой.

Но есть и косвенные признаки отправки формы, на которые браузер умеет реагировать. Как правило, отправка формы вызывает некоторые действия на странице, например:

  • переход на страницу успешной отправки формы,
  • удаление iframe, внутри которого пользователь заполнял форму,
  • удаление заполненных элементов формы или формы целиком.

Итак, можно выделить признаки, которые могут говорить об отправке:

  • навигация,
  • удаление формы,
  • удаление iframe.

Эти признаки — потенциальные триггеры к отправке. Чтобы она случилась, браузер наблюдает за процессом заполнения формы и фиксирует введённые данные во внутренних структурах. Далее при наступлении одного из трёх триггеров браузер проверяет наличие сохранённой формы с заполненными полями и фиксирует это как отправку.

Главным минусом такого подхода предполагается возможность ложных срабатываний. Ложные срабатывания приведут к тому, что браузер сохранит неверную информацию и будет показывать пользователю нерелевантные подсказки. Может проявиться и проблема быстродействия, потому что эвристики требуют от нативного и JS-кода браузера дополнительной работы.

Chromium не использует эвристики для заполнения личных данных и банковских карт, зато применяет их в случае с паролями.

Итоги


Автозаполнение — настолько же сложная и многогранная тема, как и сам веб. Есть множество вариантов форм, и их разновидности ограничены лишь полётом фантазии разработчиков сайтов. Браузеры подстраиваются и учатся обрабатывать максимально возможное число форм, чтобы облегчить пользователям взаимодействие с сайтами.

Подытожим моменты, на которые браузер обращает внимание. Вот к чему я призываю разработчиков сайтов, чтобы автозаполнение работало эффективно:

  • Используйте атрибут autocomplete и настраивайте автозаполнение согласно логике сайта.
  • Заполняйте атрибуты осмысленными значениями и не допускайте неоднозначности.
  • По возможности применяйте стандартный способ отправки.

Я постарался приоткрыть завесу сложностей вокруг автозаполнения, с которыми сталкиваюсь я и мои коллеги. Надеюсь, команды популярных браузеров и разработчики сайтов будут работать ещё теснее, чтобы делать автозаполнение корректным и удобным.

Комментарии (1)


  1. Mike-M
    10.11.2022 13:50
    +1

    Очень актуальная статья, на мой взгляд. По ощущениям, порядка 80% современных сайтов не запоминают данные обычных текстовых полей (не логинов и паролей).

    Как же надоело каждый раз копи/пастить, к примеру, 10-значный код плательщика в ЕПД!