Ответ 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
не определен на стороне браузера!