Содержание урока · 10 разделов
- chromedp spa: почему HTML пустой
- Предикат вместо сна: Poll под капотом
- Инструменты ожидания рендера
- Динамический контент go-программой: три источника задержки
- Бесконечная лента и «показать ещё»
- Порядок работы со SPA-страницей
- Состояние вместо DOM: короткий путь
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Первые десять уроков курса строились вокруг страниц, которые сервер отдаёт готовыми: навигация завершилась — разметка на месте. Витрины на фреймворках ломают эту цепочку в самом начале. Navigate честно дожидается загрузки, но загружается только каркас: корневой div, бандл скриптов и spinning-логотип. Данные приедут позже, отдельными запросами, и фреймворк соберёт из них разметку уже в браузере. Для программы, которая после Navigate сразу идёт читать узлы, это выглядит как загадочный сбой: страница есть, селектор не находит ничего. На самом деле сбоя нет — есть разрыв между «загрузка завершена» и «контент отрисован», и весь урок про то, как этот разрыв измерять и ждать. На фреймворковых витринах особенно ценен приём «перечитать селектор»: узлы в DOM появляются и исчезают по воле рендера, и действие, которое переспрашивает документ, переживает пересборку дерева без перезапуска, тогда как единственный прогон поиска мог попасть в момент, когда контейнер вынут и собирается заново. Мы возьмём инструменты из девятого урока про ожидания и десятого про оценку JavaScript и соберём из них дисциплину для живых страниц: что ждёт Navigate, почему фреймворк опаздывает, чем предикат лучше сна и где у SPA прячутся данные до рендера.
chromedp spa: почему HTML пустой#
chromedp spa-страницу начинает читать в момент, который кажется подходящим, но им не является. Navigate относится к действиям, которые всегда запускают переход и дожидаются загрузки страницы — для статического сайта этого достаточно, потому что вместе с загрузкой приезжает и контент. Для React или Vue событие загрузки означает другое: браузер получил документ и скачал скрипты. В этот момент разметки данных ещё не существует: она появится, когда скрипты запустятся, отправят запросы за данными, ответы вернутся и фреймворк прогонит их через виртуальный DOM. Задержка складывается из двух частей — сети и рендера — и ни одну из них программа не контролирует. Поэтому первое правило работы с фреймворковыми витринами звучит так: после Navigate ждут узел, которого на пустом каркасе не было. Контейнер карточек, шапка результатов, список позиций — что угодно, что рендерится только из данных. chromedp.WaitVisible по такому селектору возвращает управление, когда узел не просто найден, а имеет ненулевую рамку, то есть реально нарисован. Это уже половина дела, но не всё: контейнер может отрисоваться пустым, и тогда чтение даст ноль элементов без всякой ошибки. Оставшуюся половину закрывает предикат — условие на содержимое, а не на наличие узла.
Дождаться данных, а не контейнера
GOerr := 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-контейнера, — приносит эту ошибку сразу, не дожидаясь таймаута.
Типичная авария: в предикат пишут document.querySelector('.cards').innerText.length > 0, не подумав, что контейнер рендерится самим фреймворком и на пустом каркасе его нет. querySelector возвращает null, обращение к innerText кидает TypeError, и Poll падает с JS-исключением на первом же кадре — программа узнаёт об этом мгновенно, а не через тридцать секунд таймаута. Лечение — защитное выражение: document.querySelector('.cards') && ... возвращает false, пока контейнера нет, и Poll спокойно ждёт. Спорное мнение: проверку на null стоит писать всегда, даже когда контейнер есть в статике — переезд страницы на новый каркас не обязан предупреждать ваш парсер.
PollFunction: функция и мутации
GOvar 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, все ожидания прошли мгновенно, и программа просто не заметила, что могла бы гонять.
Если на странице видели скелетоны, значит они будут и в парсере. Проверяйте готовность по содержимому: количество узлов внутри контейнера, непустой текст первого элемента, исчезновение класса загрузки — Poll-предикатом любое из условий выражается в одну строку. Ждать фиксированное время «на всякий случай» здесь хуже, чем где-либо: скелетон живёт ровно столько, сколько едет сеть, и это не константа.
Бесконечная лента и «показать ещё»#
Каталоги с бесконечным скроллом — самый частый 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,
// ),
// )Соберите ожидание рендера
Восстановите цепочку: контейнер, предикат на количество и ограничение ожидания.
Расставьте SPA-сценарий
Соберите по порядку: контекст, навигация, ожидание контейнера, предикат на данные, обработка ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: SPA и ожидание рендера
Что дальше: с ожиданием рендера витрина становится обычной страницей курса — дальше по ней работают селекторы, клики и чтение. Двенадцатый урок доводит до конца формы: заполнение и отправку — на 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-предикату и по чтению состояния страницы — на тяжёлых витринах разница получится в секунды, и её стоит записать в конспект.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему WaitVisible прошёл, а чтение вернуло ноль элементов?
Контейнер отрисован, но пуст: так выглядит скелетон или контейнер, в который фреймворк положит данные следующим шагом. WaitVisible доказывает только наличие и видимость узла. Перенесите ожидание на содержимое — предикат с количеством узлов или текстом первого элемента — и ноль исчезнет вместе с гонкой.
Poll вернул waiting for function failed: timeout — что делать?
Тридцать секунд (или ваш WithPollingTimeout) предикат не стал истинным. Сначала проверьте предикат в консоли DevTools — часто там опечатка или проверяется не то свойство. Если выражение верное, значит данные действительно не приехали: у страницы лимиты, бан или регион. Увеличивать таймаут вслепую — путь к висящему обходу; лучше разбираться, почему условие не наступает.
Можно ли просто подождать пару секунд после Navigate?
Можно, и это самая дорогая ошибка урока. Sleep на константе проигрывает гонку в обе стороны: на быстрой сети он мёртвое время на каждой странице, на медленной — преждевременное чтение и ноль данных. chromedp.Sleep к тому же помечен в исходниках как временный, скорее всего будущий deprecated. Предикат ждёт факт и просыпается сразу после него.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Testcontainers в Go: интеграционные тесты с Docker
Курс testcontainers-go: Postgres, Redis, Kafka и Mongo в тестах через Docker, wait-стратегии, сети, параллельность, CI и итоговый стенд. 25 уроков.
- Поднимать реальные зависимости в Docker из кода теста
- Использовать модули Postgres, Redis, MongoDB, Kafka и RabbitMQ
- Ждать готовность сервиса wait-стратегиями вместо sleep
- Собирать стенды из нескольких контейнеров и своего образа