Puppeteer: 如何等待单页应用程序中的页面?

12

我正在尝试使用Puppeteer导航单页应用程序,但我面临的问题是无法等待页面加载完毕后再继续执行程序。

我填写一个表单并点击提交,根据表单内容,可能会加载不同的页面,因此我不能使用page.waitFor(Selector),因为根据输入可能会有许多不同的页面。

我尝试使用waitUntil: load、networkidle2、networkidle0、domcontentloaded等,但它们都在元素加载之前触发。

我正在尝试自动化的页面是链接。(如果您想自己检查,请选择booking reference并填写随机详细信息然后按继续。)

在链接中选择“booking-reference”后,我使用Puppeteer填写详细信息,然后按继续按钮,我无法弄清如何在不依赖选择器的情况下等待页面完全加载。


这个回答解决了你的问题吗?如何使用Puppeteer监听history.pushstate? - ggorlen
3个回答

6

我认为您应该知道这些页面是什么,并针对每个页面使用Promise.racepage.waitFor,例如:

const puppeteer = require('puppeteer');

const html = `
<html>
  <body>
    <div id="element"></div>
    <button id="button">load</button>

    <script>
      document.getElementById('button').addEventListener("click", () => {
        document.getElementById('element').innerHTML =
          '<div id="element' + (Math.floor(Math.random() * 3) + 1)  + '"></div>';
      });
    </script>
  </body>
</html>`;

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto(`data:text/html,${html}`);

  await page.click('#button');

  const element = await Promise.race([
    page.waitFor('#element1'),
    page.waitFor('#element2'),
    page.waitFor('#element3')
  ]);

  console.log(await (await element.getProperty('id')).jsonValue());
  await browser.close();
})();

谢谢!这正是我在寻找的,还有没有办法知道哪个赢了比赛?我的意思是哪个元素出现了? - Nagarjun Prasad
Promise.race只会返回一个元素:最快的元素 - 在您的情况下是区分加载了哪个页面的元素。我在代码行await (await element.getProperty('id')).jsonValue()中检测这个元素的ID,接下来就由您决定如何使用这些信息。 - Everettss

1

对于那些寻求快速答案的人,这里是主要代码:

await Promise.all([page.waitForNavigation(), el.click()]);

...其中el是指向SPA中另一页的链接,click可以是任何导致导航的事件。详情请见下文。


我认为如果您不能依赖页面内容,waitFor并不是很有用。即使您可以,在大多数情况下,这似乎比自然地对导航做出反应更不可取。幸运的是,page.waitForNavigation适用于单页应用程序。以下是使用链接上的单击事件(表单提交应该也适用)在微型原生SPA模拟中导航到页面之间的最小完整示例,它使用history API(下面的index.html)。我使用了Node 10和Puppeteer 5.4.1。

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script>
      const nav = `<a href="/">Home</a> | <a href="/about">About</a> | 
                   <a href="/contact">Contact</a>`;
      const routes = {
        "/": `<h1>Home</h1>${nav}<p>Welcome home!</p>`,
        "/about": `<h1>About</h1>${nav}<p>This is a tiny SPA</p>`,
      };
      const render = path => {
        document.body.innerHTML = routes[path] || `<h1>404</h1>${nav}`;
        document.querySelectorAll('[href^="/"]').forEach(el => 
          el.addEventListener("click", evt => {
            evt.preventDefault();
            const {pathname: path} = new URL(evt.target.href);
            window.history.pushState({path}, path, path);
            render(path);
          })
        );
      };
      window.addEventListener("popstate", e =>
        render(new URL(window.location.href).pathname)
      );
      render("/");
    </script>
  </body>
</html>

index.js:

const puppeteer = require("puppeteer");

let browser;
(async () => {
  browser = await puppeteer.launch();
  const page = await browser.newPage();

  // navigate to the home page for the SPA and print the contents
  await page.goto("http://localhost:8000");
  console.log(page.url());
  console.log(await page.$eval("p", el => el.innerHTML));

  // navigate to the about page via the link
  const [el] = await page.$x('//a[text()="About"]');
  await Promise.all([page.waitForNavigation(), el.click()]);

  // show proof that we're on the about page
  console.log(page.url());
  console.log(await page.$eval("p", el => el.innerHTML));
})()
  .catch(err => console.error(err))
  .finally(async () => await browser.close())
;

示例运行:

$ python3 -m http.server &
$ node index.js
http://localhost:8000/
Welcome home!
http://localhost:8000/about
This is a tiny SPA

如果await Promise.all([page.waitForNavigation(), el.click()]);这种模式看起来很奇怪,请参考此问题讨论串,其中解释了直觉上存在的陷阱
await page.waitForNavigation(); 
await el.click();

导致竞态条件。
与上面所示的Promise.all相同的事情可以通过以下方式完成:
const navPromise = page.waitForNavigation({timeout: 1000});
await el.click();
await navPromise;

查看此相关答案以获取更多关于使用Puppeteer导航单页应用程序的信息,包括哈希路由。


0
一个单页面应用程序等待导航并获取响应状态和数据的解决方法。无论使用fetch还是XHR来进行Ajax请求,主要思路应该是相同的。以下示例演示了如何使用fetch实现这一点。
  async spaClick (selector) {
    const res = await this.eval(selector, el => {
      window.originalFetch = window.originalFetch || window.fetch
      return new Promise(resolve => {
        window.fetch = function (...args) {
          return window.originalFetch.apply(this, args)
            .then(async response => {
              resolve({
                status: response.status,
                data: await response.clone().text()
              })

              return response
            })
        }

        el.click()
      })
    })

    if (!res) throw new Error('spaClick() Navigation triggered before eval resolves!')
    return res
  }

const puppeteer = require('puppeteer');
const url = 'http://www.faalkaart.nl';

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    // await Promise.all([
    //   page.waitForNavigation({ waitUntil: 'networkidle0' }),
    //   page.click('selector-that-triggers-navigation'),
    // ]);
    const response = await spaClick('selector-that-triggers-navigation')
    console.log(response) // {status, data}
    await browser.close();
})();


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接