edit full article archive to publication standard
Build and deploy / deploy (push) Successful in 18s
Build and deploy / deploy (push) Successful in 18s
This commit is contained in:
@@ -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 и сравнить фактические данные запроса с тем, что ожидает сервер.'),
|
||||
|
||||
Reference in New Issue
Block a user