Страница не ждет, пока другая страница завершит выполнение своих задач, прежде чем продолжить

Итак, здесь фрагмент кода:

    for (let item of items)
    {
        await page.waitFor(10000)
        await page.click("#item_"+item)
        await page.click("#i"+item)

        let pages = await browser.pages()
        let tempPage = pages[pages.length-1]

        await tempPage.waitFor("a.orange", {timeout: 60000, visible: true})
        await tempPage.click("a.orange")

        counter++
    }

page и tempPage - две разные страницы.

Случается, что page ждет 10 секунд, затем щелкает какой-то материал, который открывает вторую страницу.

Что должно произойти, так это то, что tempPage ждет элемент, щелкает его, затем страница должна ждать 10 секунд, прежде чем делать это снова и снова.

Однако, на самом деле происходит то, что page ждет 10 секунд, щелкает материал, затем начинает ждать 10 секунд, не дожидаясь завершения tempPage своих задач.

Является ли это ошибкой, или я что-то не понимаю? Как мне исправить это, так что, когда цикл цикла for снова будет установлен, он будет доступен только после того, как tempPage нажал.

Ответ 1

Как правило, вы не можете полагаться на await tempPage.click("a.orange"), чтобы приостановить выполнение до тех пор, пока tempPage не завершит выполнение своих задач ". Для супер простого кода, который выполняется синхронно, он может работать. Но в целом вы не можете полагаться на это.

Если щелчок запускает операцию Ajax или запускает анимацию CSS или запускает вычисление, которое невозможно сразу вычислить или открывает новую страницу и т.д., тогда ожидаемый результат будет асинхронным, а .click не будет ждать завершения этой асинхронной операции.

Что вы можете сделать? В некоторых случаях вы можете подключиться к коду, который запущен на странице, и ждать некоторого события, имеющего для вас значение. Например, если вы хотите дождаться выполнения операции Ajax и код на странице использует jQuery, вы можете использовать ajaxComplete для обнаружения, когда операция завершена. Если вы не можете подключиться к какой-либо системе событий, чтобы обнаружить, когда операция выполнена, вам может потребоваться опрос страницы, чтобы дождаться доказательства того, что операция выполнена.

Вот пример, который показывает проблему:

const puppeteer = require('puppeteer');

function getResults(page) {
    return page.evaluate(() => ({
        clicked: window.clicked,
        asynchronousResponse: window.asynchronousResponse,
    }));
}

puppeteer.launch().then(async browser => {
    const page = await browser.newPage();
    await page.goto("https://example.com");
    // We add a button to the page that will click later.
    await page.evaluate(() => {
        const button = document.createElement("button");
        button.id = "myButton";
        button.textContent = "My Button";
        document.body.appendChild(button);
        window.clicked = 0;
        window.asynchronousResponse = 0;
        button.addEventListener("click", () => {
            // Synchronous operation
            window.clicked++;

            // Asynchronous operation.
            setTimeout(() => {
                window.asynchronousResponse++;
            }, 1000);
        });
    });

    console.log("before clicks", await getResults(page));

    const button = await page.$("#myButton");
    await button.click();
    await button.click();
    console.log("after clicks", await getResults(page));

    await page.waitForFunction(() => window.asynchronousResponse === 2);
    console.log("after wait", await getResults(page));

    await browser.close();
});

Код setTimeout имитирует любую асинхронную операцию, запущенную щелчком.

Когда вы запустите этот код, вы увидите на консоли:

before click { clicked: 0, asynchronousResponse: 0 }
after click { clicked: 2, asynchronousResponse: 0 }
after wait { clicked: 2, asynchronousResponse: 2 }

Вы видите, что clicked сразу увеличивается два раза за два клика. Тем не менее, это займет некоторое время, прежде чем asynchronousResponse будет увеличиваться. Оператор await page.waitForFunction(() => window.asynchronousResponse === 2) проверяет страницу до тех пор, пока не будет выполнено условие, в котором мы ожидаем.


Вы упомянули в комментарии, что кнопка закрывает вкладку. Открытие и закрытие вкладок - это асинхронные операции. Вот пример:

puppeteer.launch().then(async browser => {
    let pages = await browser.pages();
    console.log("number of pages", pages.length);
    const page = pages[0];
    await page.goto("https://example.com");
    await page.evaluate(() => {
        window.open("https://example.com");
    });

    do {
        pages = await browser.pages();
        // For whatever reason, I need to have this here otherwise
        // browser.pages() always returns the same value. And the loop
        // never terminates.
        await page.evaluate(() => {});
        console.log("number of pages after evaluating open", pages.length);
    } while (pages.length === 1);

    let tempPage = pages[pages.length - 1];

    // Add a button that will close the page when we click it.
    tempPage.evaluate(() => {
        const button = document.createElement("button");
        button.id = "myButton";
        button.textContent = "My Button";
        document.body.appendChild(button);
        window.clicked = 0;
        window.asynchronousResponse = 0;
        button.addEventListener("click", () => {
            window.close();
        });
    });

    const button = await tempPage.$("#myButton");
    await button.click();

    do {
        pages = await browser.pages();
        // For whatever reason, I need to have this here otherwise
        // browser.pages() always returns the same value. And the loop
        // never terminates.
        await page.evaluate(() => {});
        console.log("number of pages after click", pages.length);
    } while (pages.length > 1);

    await browser.close();
});

Когда я запустил выше, я получаю:

number of pages 1
number of pages after evaluating open 1
number of pages after evaluating open 1
number of pages after evaluating open 2
number of pages after click 2
number of pages after click 1

Вы можете видеть, что это берет немного раньше, чем window.open() и window.close() обнаруживают эффекты.


В своем комментарии вы также писали:

Я думал, что await был в основном тем, что превращало асинхронную функцию в синхронную

Я бы не сказал, что он превращает асинхронные функции в синхронные. Это заставляет текущий код ждать, чтобы асинхронное обещание операции было разрешено или отклонено. Тем не менее, что более важно для данной проблемы, проблема заключается в том, что у вас есть две виртуальные машины, выполняющие код JavaScript: там Node, который запускает puppeteer и script, который управляет браузером, и есть сам браузер, который имеет свою собственную виртуальную машину JavaScript. Любой await, который вы используете на стороне Node, влияет только на код Node: он не влияет на код, который выполняется в браузере.

Это может запутаться, когда вы видите такие вещи, как await page.evaluate(() => { some code; }). Похоже, что все это одна штука, и все исполняются на одной и той же виртуальной машине, но это не так. puppeteer принимает параметр, переданный в .evaluate, сериализует его и отправляет в браузер, где он выполняется. Попробуйте добавить что-то вроде await page.evaluate(() => { button.click(); }); в script выше, после const button = .... Что-то вроде этого:

const button = await tempPage.$("#myButton");
await button.click();
await page.evaluate(() => { button.click(); });

В script, button определяется до page.evaluate, но вы получите ReferenceError, когда page.evaluate выполняется, потому что button не определен на стороне браузера!