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

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

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

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

Вкладки и окна: несколько страниц

В chromedp вкладки — это контексты: один контекст держит одну страницу, и вся работа с несколькими страницами сводится к заведению производных контекстов от браузерного. В этом уроке вы научитесь держать несколько страниц go-программы: одну для навигации, вторую для карточки, третью для внезапного попапа. Разберём, как NewContext порождает вкладки и что у них общее с родителем, что общего у куки и сторожа разных вкладок, а что — собственность каждой страницы, как через chromedp.Targets перечислить открытые цели и подхватить цель, которую сайт открыл сам. Отдельные темы — параллельный обход, где каждая вкладка едет в своей goroutine, и дисциплина закрытия вкладок: без cancel память растёт ровно с темпом обхода.

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

Человек открывает третий десяток вкладок, не задумываясь, и парсер хочет того же: одна страница для навигации, вторая для карточки товара, третья — внезапный попап, который сайт решил показать. Несколько страниц Go-программа держит тем же способом, что и человек, — вкладками, но под каждой вкладкой chromedp стоит конкретная единица: контекст. Именно контекст определяет, куда приедет следующий Run: у каждого контекста своя цель — target, то есть отдельная страница со своей навигацией и DOM, — и поэтому «работать с двумя вкладками» в терминах библиотеки звучит как «держать два контекста». Каркас при этом уже знаком: NewContext, Run, действия, отмена. Новыми оказываются только две вещи — откуда порождать контекст и когда его закрывать, и обе решаются связкой с родителем. Порядок изучения такой: сначала увидеть, где кончается браузер и начинается вкладка, потом научиться перечислять страницы и подхватывать новые, и в конце — параллелить и закрывать без утечек. К концу урока схема «браузер — вкладка — контекст» будет читаться в коде без усилий, а закрытие вкладки станет планироваться так же заранее, как открытие.

chromedp вкладки: NewContext от родителя#

chromedp.NewContext(parentCtx) создаёт дочерний контекст, и его судьба зависит от родителя: если в родителе уже есть браузер, дочерний контекст получает тот же браузер, но собственную вкладку — первый Run на нём не заводит процесс, а создаёт новую страницу в существующем. Каждый Execute и каждый Run с этим контекстом едут ровно в эту вкладку: навигация, клики и чтения не перелезают на соседние страницы. Второй Run по baseCtx поедет в свою вкладку, не в дочернюю: контексты не переключаются между страницами сами. Родительский контекст при этом продолжает работать со своей вкладкой, как ни в чём не бывало. Внутренности видны через chromedp.FromContext: у обоих контекстов поле Browser одно и то же, а поля Target разные — это точный ответ на вопрос, что общего и что своё. Пример manyTabs из исходников библиотеки печатает ровно это: Same browser: true, Same tab: false. Прогрев вкладки дешёвый — Run(tabCtx) без действий создаёт страницу и готовит её к работе; полезно вызвать его сразу, чтобы вкладка существовала до начала цикла, а ошибки создания ловились в одном месте. Заодно прогрев снимает вопрос «а когда именно появится вкладка»: после него — гарантированно уже.

Главная вкладка и дочерняя: две goroutines

GO
baseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()

// прогрев: создаёт браузер и первую вкладку
if err := chromedp.Run(baseCtx); err != nil {
        log.Fatal(err)
}

// вторая вкладка того же браузера
tabCtx, tabCancel := chromedp.NewContext(baseCtx)
defer tabCancel()
if err := chromedp.Run(tabCtx); err != nil {
        log.Fatal(err)
}

var wg sync.WaitGroup
wg.Add(2)
go func() { // вкладка один: свой поток действий
        defer wg.Done()
        err := chromedp.Run(baseCtx,
                chromedp.Navigate(url1),
                chromedp.WaitVisible(".list"))
        log.Println("tab1:", err)
}()
go func() { // вкладка два: независимая страница
        defer wg.Done()
        err := chromedp.Run(tabCtx,
                chromedp.Navigate(url2),
                chromedp.WaitVisible(".list"))
        log.Println("tab2:", err)
}()
wg.Wait()

Оба контекста сидят на одном браузере, но у каждого свой Target — своя страница со своим DOM. Действия одного контекста последовательны: два Run по baseCtx из разных goroutines перетасовали бы команды одной вкладки. Разные вкладки — разные потоки: wg дожидается обеих, defer cancel гасит родителя, а вместе с ним и дочернюю вкладку, если та осталась открытой.

Что общее, а что своё: куки, сторож, процесс#

Вкладки одного профиля — соседи: куки и localStorage у них общие, вход, выполненный в одной вкладке, мгновенно виден в другой, и сессия из тринадцатого урока про заголовки и куки работает на все страницы сразу. На уровне браузера делится и кэш: картинка, загруженная одной вкладкой, второй достаётся из памяти, а единый интерфейс браузера обслуживает все страницы сразу — настройки, сертификаты, разрешённые сайты. Своё у каждой вкладки — DOM, JavaScript-куча, история навигации и viewport, поэтому селектор, найденный в одной странице, в другой бессмыслен, а взгляд viewport'а одной вкладки не переезжает на соседку. Полная изоляция делается иначе: опция chromedp.WithNewBrowserContext() при NewContext заводит отдельный BrowserContext — собственную куки-банку, где сторож и хранилище не пересекаются с родительскими вкладками. Это профиль-близнец: тот же сайт выглядит незнакомцем, вход нужен заново, зато и следы не смешиваются. Новый BrowserContext уничтожается при отмене контекста, так что такая изоляция не течёт.

Targets и попап: подхватить то, что открыл сайт

GO
// инвентаризация: что сейчас открыто в браузере
infos, err := chromedp.Targets(ctx)
if err != nil {
        log.Fatal(err)
}
for _, info := range infos {
        log.Printf("%s: %s", info.Type, info.URL)
}

// попап, который сайт откроет по клику
ch := chromedp.WaitNewTarget(ctx, func(info *target.Info) bool {
        return info.Type == "page" && info.URL != ""
})
if err := chromedp.Run(ctx, chromedp.Click("#open-popup")); err != nil {
        log.Fatal(err)
}
popupCtx, popupCancel := chromedp.NewContext(ctx,
        chromedp.WithTargetID(<-ch))
defer popupCancel()

var loc string
if err := chromedp.Run(popupCtx, chromedp.Location(&loc)); err != nil {
        log.Fatal(err)
}

Targets возвращает срез target.Info по всем целям браузера: тип и адрес видны сразу, призраки и попапы находятся именно здесь. WaitNewTarget подписывается на события целей и отдаёт в канал ID появившейся страницы: сайт открыл окно — WithTargetID строит контекст прямо на него, и все дальнейшие действия едут в попап, а не в основную вкладку.

Навигация по существующим: Targets и WithTargetID#

Вкладки появляются не только от ваших NewContext: сайт открывает попап, OAuth-окно, всплывашку с капчей — и работать надо уже с тем, что открыл кто-то другой. Первая помощь — chromedp.Targets: действие возвращает срез целей с типом и адресом, по нему видно, сколько страниц открыто и какая из кого. Вторая — chromedp.WaitNewTarget: подписка, которая отдаёт в канал ID только что созданной цели по вашему предикату, — так попап ловится в момент рождения, а не поиском по списку после. Полученный ID скармливается WithTargetID: контекст строится поверх существующей вкладки, и обычный Navigate гоняет её по кругу — это и есть навигация между существующими страницами. И помните про типы: цели бывают не только страницами — iframe и worker тоже фигурируют в списке, поэтому фильтр по Type == page — привычка, которую стоит завести сразу. Тот же приём годится для восстановления: программа перезапустилась, браузер ещё жив, контекст потерян — Targets находит открытые страницы, и работа продолжается с любого из них без нового запуска.

Утечка вкладок: cancel, память и уборка#

Отмена — вторая половина жизни вкладки. chromedp.NewContext возвращает пару контекст и cancel не для красоты: отмена контекста вкладки гасит именно её — библиотека отсоединяет сессию от браузера и отдаёт команде закрытия цели, а родительский браузер при этом не трогает: он живёт, пока жив отец. Забытые cancel — самый тихий путь к росту памяти: каждая вкладка держит свой DOM и JavaScript-кучу, и мусорщик Go здесь бессилен, потому что вкладки живут в чужом процессе. Отсюда дисциплина: defer cancel для вкладки, чья жизнь равна жизни программы, и явный tabCancel сразу после шага для вкладок-однодневок из цикла. Особый случай — WithNewBrowserContext: отдельный BrowserContext удаляется вместе с контекстом, и несделанный cancel оставляет после себя целую куки-банку. Если память всё же поползла — chromedp.Targets покажет, сколько целей открыто на самом деле, и список призраков станет точным счётом вашей забывчивости.

Задачи вокруг нескольких страниц#

ЗадачаМеханизмНюанс
новая вкладкаNewContext от контекста с браузеромпервый Run создаёт вкладку, не браузер
список открытых страницchromedp.Targets(ctx)срез target.Info: тип и адрес каждой цели
подхватить попапWaitNewTarget + WithTargetIDканал отдаёт ID новой цели по предикату
изолированные кукиNewContext с WithNewBrowserContextсвой профиль: куки и сторож не общие
закрыть вкладкуcancel контекста вкладкибраузер живёт, пока жив родитель
параллельный обходвкладка на goroutineодин контекст — один поток действий

Параллельный обход: вкладка на goroutine#

Схема из первого примера масштабируется: сколько вкладок — столько независимых потоков работы, и ожидание медленной страницы больше не тормозит быструю. Правило безопасности простое: один контекст обслуживается одной goroutine — действия контекста последовательны по своей природе, и два Run по одному контексту из разных goroutines перемешают команды в пределах одной вкладки. Параллелить нужно контексты, а не действия. На этом держится и пул из девятнадцатого урока про параллельный пул: N браузеров, у каждого M вкладок, каждая единица работы — контекст в goroutine. Но вкладки — не бесплатный параллелизм: все они живут в одном процессе Chrome и делят его процессор и память, поэтому десять goroutines на одной вкладке — гонка, а пятьдесят вкладок в одном браузере — движение к OOM. Ожидания, таймауты и ретраи из предыдущего урока действуют на каждую вкладку независимо, и дедлайн одной страницы не задевает соседнюю. Разумный потолок — единицы вкладок на браузер, а дальше добавляйте браузеры: если задача не влезает в один Chrome, не мучайте вкладки сверх меры — следующий уровень масштаба — отдельные браузеры из аллокатора.

Правила гигиены вкладок#

  • контекст — вкладка: NewContext от родителя с браузером, не от context.Background() случайно
  • первый Run новой вкладки — прогрев: Run(tabCtx) без действий сразу после создания
  • cancel после шага: вкладка закрыта, память возвращена, браузер продолжает работать
  • Targets как инвентаризация: вкладки-призраки видны в списке целей
  • изоляция — WithNewBrowserContext: куки и сторож не пересекаются с основным профилем
  • параллель — вкладка на goroutine: один контекст обслуживает один поток

Проверка: что общее у вкладок

Пример manyTabs из исходников: ctx2 := chromedp.NewContext(ctx1), оба Run выполнены. Что печатает сравнение c1.Browser == c2.Browser и c1.Target == c2.Target?

Проверка: cancel дочернего контекста

Родительский контекст держит браузер, дочерний — вторую вкладку. Что произойдёт при отмене дочернего контекста?

Что сделает первый Run второй вкладки

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

baseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()
chromedp.Run(baseCtx) // браузер заведён

tabCtx, _ := chromedp.NewContext(baseCtx)
err := chromedp.Run(tabCtx)

Соберите подхват попапа

Восстановите каркас: подписка на новые цели, контекст на ID попапа и его освобождение.

ch := chromedp.(ctx, func(info *target.Info) bool { return info.Type == "page" && info.URL != "" }) popupCtx, popupCancel := chromedp.NewContext(ctx, chromedp.(<-ch)) popupCancel()

Расставьте каркас двух вкладок

Соберите по порядку: браузер с прогревом, вторая вкладка, две goroutines и ожидание.

Строки

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

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

Карточки: вкладки и окна

Термин

NewContext от контекста с браузером

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

Дочерний контекст = новая вкладка того же браузера: первый Run создаёт страницу, а не процесс. Browser наследуется, Target — собственный.

1 / 6

Что дальше: вкладки — каркас масштаба, но страницы редко живут в вакууме — они ходят в сеть. Семнадцатый урок смотрит на эту сторону: перехват сетевых запросов — события сети, тела XHR-ответов и блокировка трекеров. Восемнадцатый продолжает тему экономии — блокировка картинок и шрифтов — и собирает лёгкий профиль страницы. Практика — локально: песочница сайта работает без браузера, поэтому вкладки и попапы проверяются только на настоящем Chrome; в песочнице нет ни окна, ни цели, которую можно подхватить. Домашняя проверка на честность: откройте циклом десять вкладок без cancel, вызовите chromedp.Targets и посчитайте призраков, затем добавьте tabCancel и убедитесь, что список опустел, а память браузера вернулась к стартовой. Цель всех проверок одна: почувствовать, что вкладка — ресурс, который вы создаёте и возвращаете, а не фон, живущий сам по себе.

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

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

Вывод

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

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

  • Вкладка — это контекст: NewContext от родителя с браузером создаёт новую страницу, и все действия контекста едут в неё.
  • У вкладок общий Browser и свои Target: браузер один, страницы разные — так отвечает пример manyTabs из исходников.
  • Куки и localStorage общие для вкладок профиля; изоляция — WithNewBrowserContext, отдельная банка, уничтожаемая при отмене.
  • chromedp.Targets перечисляет открытые цели: инвентаризация, поиск призраков, восстановление после перезапуска.
  • Попап подхватывается связкой WaitNewTarget и WithTargetID: контекст строится поверх цели, открытой самим сайтом.
  • cancel контекста вкладки закрывает её; без cancel вкладки копятся, и память растёт с темпом обхода.
  • Первый Run вкладки — прогрев: Run(tabCtx) без действий создаёт страницу заранее.
  • Один контекст — одна goroutine: действия контекста последовательны, параллелят вкладки, а не действия одной страницы.
  • Пул из девятнадцатого урока — та же схема в большом масштабе: браузеры на аллокаторах и вкладки на goroutines с рабочим потолком по памяти.

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

Соберите программу из двух вкладок: браузер с прогревом, дочерний NewContext, две goroutines, каждая возит свою вкладку по трём адресам и пишет время шага. Добавьте страницу с кнопкой window.open: кликните её и подхватите попап через WaitNewTarget и WithTargetID, выведите адрес попапа. Затем испортите программу — уберите tabCancel из цикла на десять адресов — и сравните chromedp.Targets до и после: посчитайте вкладки-призраки. Финал: заведите изолированную вкладку через WithNewBrowserContext и убедитесь, что вход, выполненный в основной вкладке, в ней не виден. Лог времени шагов подскажет, даёт ли параллельность реальный выигрыш на ваших страницах.

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

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

Что сломается при двух Run по одной вкладке из разных goroutines?

Действия контекста последовательны: команды двух goroutines перемешаются в пределах одной вкладки — Navigate одной вместо Navigate другой, клик не туда, чужой DOM под чужими ожиданиями. Ошибок в духе «контекст занят» библиотека не вернёт, поэтому гонку не поймать по логам — она проявляется испорченными шагами. Правило: один контекст — один поток. Параллельную работу делает новая вкладка: NewContext от того же родителя, и у goroutine появляется собственная страница.

Чем вкладка отличается от окна?

Для chromedp — ничем существенным: и вкладка, и отдельное окно являются целью типа page со своим ID, и контекст через WithTargetID прицепляется к любой. Разница в интерфейсе браузера, а не в протоколе: целевые события, поля target.Info и действия одинаковы. Практически это значит, что вся механика урока — Targets, WaitNewTarget, cancel — работает и с настоящими окнами, и с попапами, которые сайт открыл как отдельные окна.

Как выбрать: несколько вкладок одного браузера или несколько браузеров?

Вкладки дешевле: они уже в памяти процесса, старт мгновенный, но делят процессор и память одного Chrome и общие куки профиля. Отдельный браузер изолирует всё — ресурсы, профиль, крэши — но стоит на запуск и место в памяти. Разумная лестница: вкладки для лёгкого параллелизма в пределах профиля, WithNewBrowserContext для изоляции сессий, отдельные браузеры из аллокатора для тяжёлых и независимых задач — об этом девятнадцатый урок.

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

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

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

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

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

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

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