Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~20 мин +50 XP

SPA и динамический контент

Витрина на React отдала HTML с одним пустым div — и это не поломка, а её нормальный первый кадр: на таких страницах chromedp spa-задачи решает теми же действиями, что и статические сайты, но ожидания переключаются с HTML на рендер — ждут данные, а не завершение загрузки. Разберём динамический контент go-программой: предикаты Poll, скелетоны, бесконечная лента и короткий путь через состояние страницы.

Содержание урока · 10 разделов

Первые десять уроков курса строились вокруг страниц, которые сервер отдаёт готовыми: навигация завершилась — разметка на месте. Витрины на фреймворках ломают эту цепочку в самом начале. Navigate честно дожидается загрузки, но загружается только каркас: корневой div, бандл скриптов и spinning-логотип. Данные приедут позже, отдельными запросами, и фреймворк соберёт из них разметку уже в браузере. Для программы, которая после Navigate сразу идёт читать узлы, это выглядит как загадочный сбой: страница есть, селектор не находит ничего. На самом деле сбоя нет — есть разрыв между «загрузка завершена» и «контент отрисован», и весь урок про то, как этот разрыв измерять и ждать. На фреймворковых витринах особенно ценен приём «перечитать селектор»: узлы в DOM появляются и исчезают по воле рендера, и действие, которое переспрашивает документ, переживает пересборку дерева без перезапуска, тогда как единственный прогон поиска мог попасть в момент, когда контейнер вынут и собирается заново. Мы возьмём инструменты из девятого урока про ожидания и десятого про оценку JavaScript и соберём из них дисциплину для живых страниц: что ждёт Navigate, почему фреймворк опаздывает, чем предикат лучше сна и где у SPA прячутся данные до рендера.

chromedp spa: почему HTML пустой#

chromedp spa-страницу начинает читать в момент, который кажется подходящим, но им не является. Navigate относится к действиям, которые всегда запускают переход и дожидаются загрузки страницы — для статического сайта этого достаточно, потому что вместе с загрузкой приезжает и контент. Для React или Vue событие загрузки означает другое: браузер получил документ и скачал скрипты. В этот момент разметки данных ещё не существует: она появится, когда скрипты запустятся, отправят запросы за данными, ответы вернутся и фреймворк прогонит их через виртуальный DOM. Задержка складывается из двух частей — сети и рендера — и ни одну из них программа не контролирует. Поэтому первое правило работы с фреймворковыми витринами звучит так: после Navigate ждут узел, которого на пустом каркасе не было. Контейнер карточек, шапка результатов, список позиций — что угодно, что рендерится только из данных. chromedp.WaitVisible по такому селектору возвращает управление, когда узел не просто найден, а имеет ненулевую рамку, то есть реально нарисован. Это уже половина дела, но не всё: контейнер может отрисоваться пустым, и тогда чтение даст ноль элементов без всякой ошибки. Оставшуюся половину закрывает предикат — условие на содержимое, а не на наличие узла.

Дождаться данных, а не контейнера

GO
err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com/catalog"),
        chromedp.WaitVisible(".cards"),
        chromedp.Poll(
                "document.querySelectorAll('.card').length >= 20",
                nil,
                chromedp.WithPollingTimeout(15*time.Second),
        ),
)

Navigate дожидается загрузки каркаса, WaitVisible — контейнера с ненулевой рамкой, а Poll — двадцати карточек внутри него. Предикат переоценивается браузером постоянно, программа спит в действии и просыпается только на факт. nil вместо приёмника означает «только ждать»; таймаут ограничивает ожидание пятнадцатью секундами и при истечении вернёт ErrPollingTimeout.

Предикат вместо сна: Poll под капотом#

chromedp.Poll строит из выражения предикат — в исходниках он оборачивается в return (выражение); — и передаёт браузеру на постоянную переоценку. Режимов три. Дефолтный raf исполняет предикат на каждом кадре requestAnimationFrame — самый тугой опрос, подходит для наблюдения за стилями и быстрыми изменениями. WithPollingInterval переключает на таймер с указанным интервалом — разумный выбор для данных, которые едут по сети и не меняются чаще раза в секунду. WithPollingMutation запускает переоценку на каждую мутацию DOM — идеален для лент, где элементы появляются событиями. Три опции настраивают ожидание: WithPollingTimeout ставит потолок, по умолчанию тридцать секунд, ноль отключает его совсем; при истечении действие возвращает ошибку waiting for function failed: timeout. Приёмник получает значение предиката, когда тот наконец вернул истину, — так одним действием можно и дождаться, и забрать результат; тип приёмника подбирается по форме значения: логическое выражение — nil или *bool, число — int, строка — string. Две границы стоит помнить заранее. Предикат не переживает навигацию: если страница переехала, Poll вернёт ошибку уничтоженного контекста исполнения, а не продолжит ждать. И предикат, который кидает исключение — например, обращается к свойству null-контейнера, — приносит эту ошибку сразу, не дожидаясь таймаута.

PollFunction: функция и мутации

GO
var cards int
err := chromedp.Run(ctx,
        chromedp.WaitVisible(".cards"),
        chromedp.PollFunction(
                "() => document.querySelectorAll('.card').length",
                &cards,
                chromedp.WithPollingMutation(),
        ),
)
fmt.Println("карточек:", cards)

PollFunction оборачивает функцию: пока она возвращает ложное значение, ожидание продолжается, первое истинное значение уходит в приёмник. Ноль карточек — ложь, поэтому действие дождётся первой же отрисованной позиции и вернёт её количество. Режим мутаций переоценивает функцию на каждом изменении DOM — без таймеров и лишних кадров.

Инструменты ожидания рендера#

ИнструментЧто ждётКогда брать
WaitVisibleузел с ненулевой рамкойконтейнер данных уже отрисован
Poll с выражениемистинность выраженияколичество узлов, текст, флаг готовности
PollFunction + Mutationвызов функции на каждой мутации DOMленты и подгрузки, где элементы приходят событиями
Evaluateничего — разовый снимокчтение состояния после того, как готовность доказана

Динамический контент go-программой: три источника задержки#

Динамический контент go-программа ждёт тем точнее, чем яснее, откуда берётся пауза. Первый источник — сеть: фреймворк отправляет запросы за данными после запуска, и ответы могут ехать секунды на тяжёлых витринах; этот этап — территория таймерного или интервального опроса. Второй — рендер: фреймворк собирает разметку постепенно, и на её месте часто стоит скелетон — серые прямоугольники, изображающие будущие карточки. Скелетон опасен тем, что он узел реальный: WaitVisible на нём проходит мгновенно, а данных внутри ноль, поэтому для витрин со скелетонами ожидание обязано смотреть на содержимое, а не на контейнер. Третий источник — гидрация: разметка уже в документе, а скрипты ещё навешивают поведение; для чтения текстов это не мешает, но клики до гидрации уходят в никуда. Режим опроса подбирается под источник: данные из сети — интервал, рост списка — мутации, стили и анимации — кадры. Когда источник неясен, начинают с мутаций: любой рендер фреймворка — это мутация DOM, и предикат сработает на первом же изменении, сделавшем условие истинным. Разобраться, куда уходит время, помогает DevTools на той же странице: длинная пауза перед первым XHR — это сеть или бэкенд, долгий рендер после ответа — фреймворк; тот же разбор работает и в обратную сторону — если данные отрисовались до вашего WaitVisible, все ожидания прошли мгновенно, и программа просто не заметила, что могла бы гонять.

Бесконечная лента и «показать ещё»#

Каталоги с бесконечным скроллом — самый частый SPA-сценарий после первого экрана. Схема программы: доехать до низа страницы прокруткой, дождаться новой порции узлов, повторить. Прокрутка делается Evaluate с window.scrollTo до document.body.scrollHeight, а ожидание порции — PollFunction с предикатом, сравнивающим текущее количество карточек с зафиксированным раньше. Конец ленты распознаётся по остановке роста: после очередного скролла счётчик не сдвинулся N попыток подряд — данные кончились, и цикл выходит. Здесь живёт одна тонкость виртуализированных списков: экономные фреймворки удаляют узлы, уехавшие за экран, поэтому собирать тексты «в конце, разом» нельзя — данные читают на каждом шаге цикла, пока они в DOM, либо берут их из состояния страницы, о чём ниже. Для витрин с кнопкой «показать ещё» всё проще: тот же цикл, но вместо скролла — Click по кнопке и предикат на рост счётчика. Если сайт позволяет, проверьте и альтернативу: у многих лент есть пагинация с адресами страниц, и она надёжнее скролла — этой теме целиком посвящён двадцать первый урок про таблицы и пагинации.

Порядок работы со SPA-страницей#

  • Navigate дожидается загрузки каркаса — дальше вы одни с фреймворком
  • WaitVisible на узел, которого нет в статике: контейнер, шапка, список
  • Poll или PollFunction на содержимое: количество узлов, текст, отсутствие индикатора загрузки
  • режим опроса под источник задержки: сеть — интервал, лента — мутации, стили — кадры
  • для лент — цикл скролл-ожидание-чтение с проверкой остановки роста
  • результат предиката забирайте в приёмник — готовность и данные одним действием
  • ни одного Sleep: скелетон и сеть не живут по расписанию

Состояние вместо DOM: короткий путь#

У SPA есть задняя дверь, мимо которой проходят половина парсеров: данные существуют до того, как стали разметкой. Фреймворк держит их в памяти — в состоянии приложения, в глобальных объектах, которые сайт кладёт для себя: window.__INITIAL_STATE__, объект конфигурации, кэш запросов. Evaluate из десятого урока читает это состояние напрямую и возвращает его в Go JSON-ом — без ожидания рендера, без скелетонов, одним обменом. Для тяжёлых витрин это не просто ускорение: сериализованное состояние стабильнее разметки, которую дизайнер завтра перестроит. Имена глобалов у фреймворков свои: __NEXT_DATA__ у Next.js, __NUXT__ у Nuxt, у самописных приложений чаще всего __INITIAL_STATE__ — подсмотреть их можно в консоли DevTools, начав печатать window.__ и глядя на автодополнение. Границы пути: состояние не документировано, его структура меняется без предупреждения, и у части сайтов оно не глобальное — тогда остаётся классический путь с ожиданием рендера. Второй короткий путь — вообще не ждать рендер, а слушать сеть: если данные едут XHR-ом, их можно перехватывать ответами до того, как фреймворк их распакует, — механика семнадцатого урока про перехват запросов. Выбирайте по задаче: разовый сбор — DOM с предикатом, промышленный обход — состояние или сеть. А для статических сайтов без скриптов всё это лишнее: смежный курс разбирает чистый HTTP-путь, который там дешевле.

Проверка: что дожидается Navigate

Navigate вернулся без ошибки, но селектор карточек не находит ничего. Что произошло?

Проверка: режимы и таймаут Poll

Какой режим опроса у Poll по умолчанию и какой потолок ожидания?

Что окажется в cards

Прочитайте код и выберите, что он выведет:

// на странице сейчас 0 карточек, через 3 секунды фреймворк отрисует 12
var cards int
// err := chromedp.Run(ctx,
//   chromedp.PollFunction(
//     "() => document.querySelectorAll('.card').length",
//     &cards,
//   ),
// )

Соберите ожидание рендера

Восстановите цепочку: контейнер, предикат на количество и ограничение ожидания.

err := chromedp.Run(ctx, chromedp.Navigate(url), chromedp.(".cards"), chromedp.Poll( "document.querySelectorAll('.card').length >= 20", , chromedp.WithPollingTimeout(15*time.Second), ), )

Расставьте SPA-сценарий

Соберите по порядку: контекст, навигация, ожидание контейнера, предикат на данные, обработка ошибки.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: SPA и ожидание рендера

Термин

Что дожидается Navigate на SPA

нажмите, чтобы перевернуть
Определение

Событие загрузки: документ и скрипты на месте. Данных фреймворка ещё нет — их приезд и рендер ждут отдельными действиями.

1 / 6

Что дальше: с ожиданием рендера витрина становится обычной страницей курса — дальше по ней работают селекторы, клики и чтение. Двенадцатый урок доводит до конца формы: заполнение и отправку — на SPA-витринах формы почти всегда с JS-валидацией, поэтому без этого урока логин не соберётся. Если данные не нужны в DOM, семнадцатый урок покажет перехват запросов, а двенадцатый же — как дождаться подтверждения отправки. Практика — локально: нужен установленный Go и Chrome, песочница сайта исполняет тематическую программу на чистом Go без браузера, поэтому предикаты и мутации вы увидите только в своём терминале. Домашняя проверка на честность: откройте любую витрину, выключите сеть и посмотрите, как выглядит каркас, — именно его видит Navigate.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Navigate на SPA дожидается загрузки каркаса, а не данных: разметку собирает фреймворк после загрузки.
  • После Navigate ждут узел, которого нет в статике, — контейнер с ненулевой рамкой.
  • Poll строит предикат из выражения и ждёт первую истину; значение прилетает в приёмник.
  • Режимы опроса: raf по умолчанию, WithPollingInterval для сетевых данных, WithPollingMutation для лент.
  • WithPollingTimeout — 30 секунд по умолчанию, ноль отключает; истечение даёт ErrPollingTimeout.
  • Предикат не переживает навигацию и падает на исключении — защита на null обязательна.
  • Скелетон — реальный узел без данных: готовность доказывает содержимое, а не контейнер.
  • Состояние страницы читается Evaluate до рендера; перехват XHR — альтернатива без ожидания разметки.
  • Имена глобального состояния у фреймворков свои — __NEXT_DATA__, __NUXT__; подсматриваются в консоли автодополнением.

Домашнее задание

Возьмите витрину с фреймворком и соберите программу: Navigate, WaitVisible на контейнер данных, Poll с предикатом на минимальное количество карточек и чтение первых заголовков. Затем добавьте цикл бесконечного скролла с подсчётом карточек до остановки роста и логом по каждой порции. В конце сломайте предикат — уберите защитную проверку на null-контейнер — и прочитайте текст ошибки, чтобы узнать её в проде. Финальный штрих: сравните время до первых данных по DOM-предикату и по чтению состояния страницы — на тяжёлых витринах разница получится в секунды, и её стоит записать в конспект.

Личные заметкиПусто
Хранятся только в вашем браузере (Chromedp · SPA и динамический контент)

Вопросы и ответы по уроку

Почему WaitVisible прошёл, а чтение вернуло ноль элементов?

Контейнер отрисован, но пуст: так выглядит скелетон или контейнер, в который фреймворк положит данные следующим шагом. WaitVisible доказывает только наличие и видимость узла. Перенесите ожидание на содержимое — предикат с количеством узлов или текстом первого элемента — и ноль исчезнет вместе с гонкой.

Poll вернул waiting for function failed: timeout — что делать?

Тридцать секунд (или ваш WithPollingTimeout) предикат не стал истинным. Сначала проверьте предикат в консоли DevTools — часто там опечатка или проверяется не то свойство. Если выражение верное, значит данные действительно не приехали: у страницы лимиты, бан или регион. Увеличивать таймаут вслепую — путь к висящему обходу; лучше разбираться, почему условие не наступает.

Можно ли просто подождать пару секунд после Navigate?

Можно, и это самая дорогая ошибка урока. Sleep на константе проигрывает гонку в обе стороны: на быстрой сети он мёртвое время на каждой странице, на медленной — преждевременное чтение и ноль данных. chromedp.Sleep к тому же помечен в исходниках как временный, скорее всего будущий deprecated. Предикат ждёт факт и просыпается сразу после него.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Testcontainers25 уроков~8 чпрактика25 уроков уже доступны

Testcontainers в Go: интеграционные тесты с Docker

Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.

  • Поднимать реальные зависимости в Docker из кода теста
  • Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
  • Ждать готовность сервиса wait-стратегиями вместо sleep
  • Собирать стенды из нескольких контейнеров и своего образа