一文带你轻松学会 Puppeteer 并实际运用

3,480 阅读6分钟

介绍

  • 在实际的项目中有很多需要截图的场景,例如分享图片(图片是某个页面动态的数据)、打卡、保存 HTML 页面等。
  • 文末有对整个库的一个归纳总结,哪些是什么(属性、方法...),哪个属性属于哪个类,以及这些都是怎么调用的。

Puppeteer 是什么

Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。

Puppeteer 能做什么

  • 可以在浏览器中手动执行的绝大多数操作都可以使用 Puppeteer 来完成! 例如:
  • 生成页面截图、PDF
  • 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
  • 自动提交表单,进行 UI 测试,键盘输入等。
  • 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的 Chrome 中执行测试。
  • 捕获网站的 timeline trace,用来帮助分析性能问题。
  • 测试浏览器扩展。

官方演示地址: try-puppeteer.appspot.com/

开始使用

  • Puppeteer 至少需要 Node v6.4.0,下面的示例使用 async / await,它们仅在 Node v7.6.0 或更高版本中被支持。

截取为图片

// example.js
const puppeteer = require('puppeteer');
(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com');
    await page.screenshot({path: 'example.png'});
    await browser.close();
})();

// 执行
node example.js
  • 创建自执行函数;
  • 使用默认配置创建一个浏览器的实例;
  • 打开一个链接,并截图配置保存地址及名称;
  • 关闭浏览器。

截取为PDF

// hn.js
const puppeteer = require('puppeteer');
(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle2'});
    await page.pdf({path: 'hn.pdf', format: 'A4'});
    await browser.close();
})();

// 执行
node hn.js

在打开的页面中执行脚步

// get-dimensions.js
const puppeteer = require('puppeteer');
(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://example.com');
    // Get the "viewport" of the page, as reported by the page.
    const dimensions = await page.evaluate(() => {
        return {
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight,
            deviceScaleFactor: window.devicePixelRatio
        };
    });
    console.log('Dimensions:', dimensions);
    await browser.close();
})();

// 执行
node get-dimensions.js

截图功能扩展

launch 常用配置

  • headless:是否使用无头模式,默认为 true
  • executablePath:使用的 chrome 路径;
  • defaultViewport:默认视图的配置(width、height、deviceScaleFactor、isMobile 等);
  • 更多 launch 配置

screenshot 常用配置

  • path:保存的路径;
  • type:保存图片的类型(png | jpeg | webp);
  • fullpage:是否截取全屏;HTML body 的高度,如果不是body滚动,内容无法被完整截取!!!
  • omitBackgroud:隐藏默认的白色背景,背景透明。
  • 更多 screenshot 配置

page 页面设置

  1. 设置打开页面的设备类型
console.log('puppeteer.devices',puppeteer.devices)
await page.emulate(puppeteer.devices['Galaxy S8'])
await page.goto('https://www.baidu.com')

full-baidu.jpg 2. 获取输入框并输入内容,截取操作结果

await page.type('#index-kw', 'puppeteer')
await page.click('#index-bn')
  1. 截取指定元素
/** content 截取指定元素 querySelector */
const content = await page.$('.poster-preview-content');
await content.screenshot({
    path: './full-baidu.jpg',
    type: 'png', // png | jpeg | webp
    // fullPage: true,
    omitBackground: true, // 隐藏默认的白色背景,背景透明
})
  1. 获取页面样式,动态设置截图的宽高等

const dom = await page.$eval('.poster-preview-content',(x) => {
    return JSON.parse(JSON.stringify(window.getComputedStyle(x)))
})
console.log('dom', dom.width, dom.height)
await page.setViewport({
    width: Number(dom.width.slice(0, -2)), 
    height: Number(dom.height.slice(0, -2))
})
  1. 设置截图等待时间
try {
    await page.waitForNavigation({ timeout: 1000 })
} catch (e) {
    console.log('err', e)
}

API 介绍

Browser

const browser = await puppeteer.launch({
    headless: true
})
  1. 事件
  1. 方法

Page

  • Page 提供操作一个 tab 页或者 extension background page 的方法。一个 Browser 实例可以有多个 Page 实例。
  • Page 拥有很多的事件和方法,例如上文使用的 gotowaitForNavigationscreenshot 等等都是常用的方法,更多请查阅官网 Page

Worker

  • Worker 类表示一个 WebWorker。在页面对象上 workercreated 和 workerdestroyed 事件被触发,以标识 worker 的生命周期。
page.on('workercreated', worker => console.log('Worker created: ' + worker.url()));
page.on('workerdestroyed', worker => console.log('Worker destroyed: ' + worker.url()));

console.log('Current workers:');
for (const worker of page.workers()){
  console.log('  ' + worker.url());
}
  1. 方法

KeyboardMouseTouchscreen

  • 跟据名称我们就能清楚知道这是和键盘、鼠标、触摸相关的 API,当我们在浏览器中有这些操作的时候就需要用到对应的 API 了。
// Keyboard
await page.keyboard.type('Hello World!');
await page.keyboard.press('ArrowLeft');
await page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
await page.keyboard.press('ArrowLeft');
await page.keyboard.up('Shift');
await page.keyboard.press('Backspace');
// 结果字符串最终为 'Hello!'

// 使用 ‘page.mouse’ 追踪 100x100 的矩形。
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.move(100, 100);
await page.mouse.move(100, 0);
await page.mouse.move(0, 0);
await page.mouse.up();

// touchscreen
touchscreen.tap(x, y)

Tracing

await page.tracing.start({path: 'trace.json'});
await page.goto('https://www.google.com');
await page.tracing.stop();

Dialog

const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  page.on('dialog', async dialog => {
    console.log(dialog.message());
    await dialog.dismiss();
    await browser.close();
  });
  page.evaluate(() => alert('1'));
});

 ConsoleMessage

Frame

ExecutionContext

  • 该类表示一个 JavaScript 执行的上下文。 Page 可能有许多执行上下文:

    • 每个 frame 都有 "默认" 的执行上下文,它始终在将帧附加到 DOM 后创建。该上下文由 frame.executionContext() 方法返回。
    • Extensions 的内容脚本创建了其他执行上下文。
  • 除了页面,执行上下文可以在 workers 中找到。

JSHandle

  • JSHandle 表示页面内的 JavaScript 对象。 JSHandles 可以使用 page.evaluateHandle 方法创建。
const windowHandle = await page.evaluateHandle(() => window);
// ...
  • JSHandle 可防止引用的 JavaScript 对象被垃圾收集,除非是句柄 disposed。 当原始框架被导航或父上下文被破坏时,JSHandles 会自动处理。

  • JSHandle 实例可以使用在 page.$eval()page.evaluate() 和 page.evaluateHandle 方法。

ElementHandle

注意 ElementHandle 类继承自 JSHandle

  • ElementHandle 表示一个页内的 DOM 元素。ElementHandles 可以通过 page.$ 方法创建。
const puppeteer = require('puppeteer');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.goto('https://google.com');
  const inputElement = await page.$('input[type=submit]');
  await inputElement.click();
  // ...
});
  • 除非处理了句柄 disposed,否则 ElementHandle 会阻止垃圾收集中的 DOM 元素。 ElementHandles 在其原始帧被导航时将会自动处理。

  • ElementHandle 实例可以在 page.$eval() 和 page.evaluate() 方法中作为参数。

RequestResponse

  • 请求相关,当页面发起请求、请求响应时触发。

SecurityDetails

  • SecurityDetails 类表示通过安全连接收到响应时的安全性详细信息。
page.on('response', res => {
    console.log('SecurityDetails', res.securityDetails()?.protocol())
})

Target

const browser = await puppeteer.launch({
    headless: true
})
const page = await browser.newPage()
console.log('Target',page['target']())

CDPSession

  • CDPSession 实例用于与 Chrome Devtools 协议的原生通信:
    • 协议方法可以用 session.send 方法调用。
    • 协议事件可以通过 session.on 方法订阅。
const client = await page.target().createCDPSession()await client.send('Animation.enable');
client.on('Animation.animationCreated', () => console.log('Animation created!'));
const response = await client.send('Animation.getPlaybackRate');
console.log('playback rate is ' + response.playbackRate);
await client.send('Animation.setPlaybackRate', {
    playbackRate: response.playbackRate / 2
});

Coverage

  • Coverage 收集相关页面使用的 JavaScriptCSS 部分的信息。

TimeoutError

归纳总结

  1. EventWorker/Dialog/ConsoleMessage/Frame/ExecutionContext[frame/worker]/Request/Response
  2. page PropertyAccessibility/Keyboard/Mouse/Touchscreen/Tracing/Coverage
  3. page FunctionTarget
  4. page.target() FunctionCDPSession
  5. Event Response 返回值 FunctionSecurityDetails
  6. ExecutionContextframe/worker
  7. JSHandle、ElementHandle

事例代码完整版

const puppeteer = require('puppeteer');

/** BrowserFetcher */
// const browserFetcher = puppeteer.createBrowserFetcher();
(async () => {
    const browser = await puppeteer.launch({
        headless: true
    })
    const version = await browser.version()
    console.log('browser.version()', version)
    const page = await browser.newPage()
    /** Event Worker/Dialog/ConsoleMessage/Frame/ExecutionContext[frame/worker]/Request/Response */
    // page.on('workercreated', frame => console.log('worker: ' + worker.type()));
    /** ExecutionContext frame/worker */
    // page.on('frameattached', frame => console.log('console: ' + frame.ExecutionContext()));
    /** JSHandle */
    // const windowHandle = await page.evaluateHandle(() => window);
    /** ElementHandle */
    // const inputElement = await page.$('input[type=submit]');
    // await inputElement.click();

    /**  page Property Accessibility/Keyboard/Mouse/Touchscreen/Tracing/Coverage */
    // page['accessibility'/'keyboard'/'mouse'/'touchscreen'/'tracing'/'coverage']
    /**  page Function Target */
    console.log('Target',page['target']())
    /**  page.target() Function CDPSession */
    // const client = await page.target().createCDPSession()

    /** SecurityDetails -> Event  Response 返回值 Function */
    // page.on('response', res => {
    //     console.log('SecurityDetails', res.securityDetails()?.protocol())
    // })
    // console.log('puppeteer.devices',puppeteer.devices)
    await page.emulate(puppeteer.devices['iPhone 8 Plus'])
    await page.goto('https://www.baidu.com')
    /**   输入框 */
    // await page.type('#index-kw', 'puppeteer')
    // await page.click('#index-bn')
    /**  超时 TimeoutError */
    // try {
    //     await page.waitForNavigation({ timeout: 1000 })
    // } catch (e) {
    //     console.log('err', e)
    // }
    /**  获取样式 */
    // const dom = await page.$eval('.poster-preview-content',
    //     (x) => {return JSON.parse(JSON.stringify(window.getComputedStyle(x)))})
    // console.log('dom', dom.width, dom.height)
    // await page.setViewport({width: Number(dom.width.slice(0, -2)), height: Number(dom.height.slice(0, -2))})
    // await page.goto('https://mobiledev.hongsong.club/hs-hybrid-app/performance/posterImage?partyCode=p_B480O59G1002&stationId=s_A148H2B84O02&image=https://hs-headlesschrome.hongsong.club/screenshot/FaFEe6Z3H6G4wNht6e2hZwsNE33ry4jE.jpg&screenShot=true')
    /** content 截取指定元素 querySelector */
    // const content = await page.$('.poster-preview-content');
    /** page 截取body */
    await page.screenshot({
        path: './full-baidu.jpg',
        type: 'png', // png | jpeg | webp
        // fullPage: true,
        omitBackground: true, // 隐藏默认的白色背景,背景透明
    })
    await browser.close();
})()

往期精彩

「点赞、收藏和评论」

❤️关注+点赞收藏+评论+分享❤️,手留余香,谢谢🙏大家。