edit full article archive to publication standard
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 23:08:19 +03:00
parent 440c8721dc
commit 7c5b19c960
28 changed files with 583 additions and 39 deletions
+3 -3
View File
@@ -149,7 +149,7 @@ const practiceArticle = createRevision(
readingMinutes: 9,
},
[
paragraph('В старом интерфейсе блок заказа часто обновляется без перезагрузки страницы. После ответа Ajax мы заново вызываем <code>mountOrderForm</code>, потому что так проще, чем помнить все места, где изменилась разметка. Через неделю один клик по кнопке уходит двумя запросами. Через месяц — тремя. Ошибка неприятна не из-за консоли: одна пользовательская команда может несколько раз изменить состояние на сервере.'),
paragraph('В старом интерфейсе блок заказа часто обновляется без перезагрузки страницы. После ответа Ajax мы заново вызываем <code>mountOrderForm</code>, потому что так проще, чем помнить все места, где изменилась разметка. Через неделю один клик по кнопке уходит двумя запросами. Через месяц — тремя. Цена ошибки — один клик создаёт несколько запросов и может несколько раз изменить состояние на сервере.'),
paragraph('Главный вопрос здесь узкий: как написать инициализацию jQuery-виджета так, чтобы её можно было вызвать повторно и на кнопке оставался ровно один наш обработчик? Не будем переписывать весь legacy-код. Достаточно сделать явный контракт у одной функции <code>mount</code> и проверить его в браузере.'),
heading('Почему обработчик умножается'),
paragraph('Метод <code>.on()</code> привязывает обработчик к текущей выбранной коллекции. Если один и тот же код вызвать ещё раз, старый обработчик сам не исчезает. Официальная документация jQuery отдельно отмечает, что один обработчик можно привязать к элементу несколько раз. Поэтому проблема не в Ajax как таковом, а в функции, которая при каждом вызове только добавляет новое событие.'),
@@ -272,7 +272,7 @@ const mechanismArticle = createRevision(
readingMinutes: 9,
},
[
paragraph('Каталог отрисовал новую страницу товаров через Ajax: <code>#products</code> получил свежий HTML, карточки на экране есть, но кнопка «В корзину» больше не реагирует. Первая реакция обычно понятна — ещё раз вызвать функцию, которая вешает click. После пары таких правок появляются уже две проблемы: у новых кнопок нет обработчика до следующей инициализации, а у старых он начинает дублироваться.'),
paragraph('Каталог отрисовал новую страницу товаров через Ajax: <code>#products</code> получил свежий HTML, карточки на экране есть, но кнопка «В корзину» больше не реагирует. Первая реакция обычно понятна — ещё раз вызвать функцию, которая вешает click. После пары таких правок появляются уже две проблемы: у новых кнопок нет обработчика до следующей инициализации, а у старых он начинает дублироваться. Цена ошибки — один пользовательский клик создаёт несколько запросов.'),
paragraph('Главный вопрос этой заметки: почему обработчик пропадает после <code>.html()</code> и как выбрать делегирование так, чтобы оно пережило замену карточек? Здесь важно не запомнить «вешай всё на document», а увидеть, на каком DOM-узле реально хранится обработчик и какой узел переживает обновление.'),
heading('Что делает .html() с прежней разметкой'),
paragraph('Когда <code>.html(строка)</code> задаёт новое содержимое, jQuery полностью заменяет прежних потомков контейнера. Документация отдельно предупреждает: перед заменой jQuery удаляет из дочерних элементов данные и обработчики событий. Поэтому прямой click на старой кнопке не «ломается» — он остаётся на старом DOM-узле, которого больше нет. Новая кнопка похожа внешне, но для браузера это другой объект.'),
@@ -393,7 +393,7 @@ const fieldArticle = createRevision(
readingMinutes: 10,
},
[
paragraph('Форма заказа в старом интерфейсе может отправиться дважды не только из-за двойного клика. Пользователь нажал Enter, скрипт повторно повесил submit, кнопка осталась активной до ответа или код решил «на всякий случай» повторить запрос. В браузере это выглядит как маленькая ошибка. На сервер могут уйти два одинаковых POST, а последствия уже зависят от предметной области.'),
paragraph('Форма заказа в старом интерфейсе может отправиться дважды не только из-за двойного клика. Пользователь нажал Enter, скрипт повторно повесил submit, кнопка осталась активной до ответа или код решил «на всякий случай» повторить запрос. В браузере это выглядит как маленькая ошибка. На сервер могут уйти два одинаковых POST, а последствия уже зависят от предметной области. Цена ошибки — дубль операции, платежа или заявки.'),
paragraph('Главный вопрос здесь такой: как сделать Ajax-форму, которая допускает один активный запрос в текущем DOM-экземпляре, честно показывает ошибку и в любом исходе возвращает интерфейс в готовое состояние? Это не заменяет серверную защиту операции. Зато убирает повторную отправку, созданную именно фронтенд-кодом, и даёт понятную точку диагностики.'),
heading('Сначала определим, что именно отправляет форма'),
paragraph('Метод <code>.serialize()</code> строит URL-кодированную строку из успешных контролов формы. Практическое следствие простое: у поля должен быть <code>name</code>, выключенные поля не попадут в набор, неотмеченный checkbox тоже не попадёт, а файл через <code>.serialize()</code> не отправится. Поэтому перед переписыванием обработчика стоит открыть Network и сравнить фактические данные запроса с тем, что ожидает сервер.'),