很早很早以前,前端就有了對後端環境調用瀏覽器頁面功能的需求,最多的應用場景有兩個javascript
市場上出現過不少優秀的解決方案,在 Puppeteer 出現以前最經常使用的是 PhantomJS 和 selenium-webdriver,但兩個庫有個共同特色——環境安裝複雜,API 調用不語義化。2017 年 Chrome 團隊連續放了兩個大招 Headless Chrome 和對應的 NodeJS API Puppeteer,直接讓 PhantomJS 和 Selenium IDE for Firefox 做者宣佈不必繼續維護其產品html
如同其 github 項目介紹:Puppeteer 是一個經過 DevTools Protocol 控制 headless chrome 的 high-level Node 庫,提供了高度封裝、使用方便的 API 來模擬用戶在頁面的操做、對瀏覽器事件作出響應等前端
手動能夠在瀏覽器上作的大部分行爲 Puppeteer 都能經過 API 真實模擬,API 使用很是簡單,看下官網對網頁截圖的示例java
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();
})();
複製代碼
實現網頁截圖就這麼簡單,用過 selenium-webdriver 的同窗看了會流淚,官方提供了一個 playground,能夠快速體驗一下git
雖然 Puppeteer API 足夠簡單,但若是是從 webdriver 流轉過來的同窗會很不適應,主要是在 webdirver 中操做網頁更多的是從程序的視角,而在 Puppeteer 中網頁瀏覽者的視角。舉個簡單的例子,對一個表單的 input 作輸入github
使用 webdriver 流程web
const input = await driver.findElement(By.id('kw'));
await input.sendKeys('test');
複製代碼
使用 Puppeteer 流程chrome
await page.focus('#kw');
await page.keyboard.type('test');
複製代碼
甚至能夠簡化爲一條語句:向 input 輸入字符npm
await page.type('#kw', 'test');
複製代碼
能夠看到 Puppeteer 的使用流程幾乎是在模擬人的操做,在使用過程當中能夠感覺區別,會發現 Puppeteer 的使用天然不少後端
npm i puppeteer
複製代碼
比起 PhantomJS 和 selenium-webdriver 實在簡單了太多,安裝 Puppeteer 時會下載最新版本的 Chromium,從 1.7 開始 Puppeteer 每次發佈還會有一個 puppeteer-core 發佈,相對於 puppeteer 有兩個區別
PUPPETEER_* env
變量如同 Node.js 啓動能夠設置環境變量,puppeteer 也支持特定的環境變量
HTTP_PROXY
,HTTPS_PROXY
,NO_PROXY
- 定義用於下載和運行 Chromium 的 HTTP 代理設置。PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
- 請勿在安裝步驟中下載綁定的 Chromium。PUPPETEER_DOWNLOAD_HOST
- 覆蓋用於下載 Chromium 的 URL 的主機部分。PUPPETEER_CHROMIUM_REVISION
- 在安裝步驟中指定一個你喜歡 puppeteer 使用的特定版本的 Chromium。PUPPETEER_EXECUTABLE_PATH
- 指定一個 Chrome 或者 Chromium 的可執行路徑,會被用於puppeteer.launch
。具體關於可執行路徑參數的意義,可參考puppeteer.launch([options])
。
Puppeteer API 設計和瀏覽器層次相對應(淺色框體內容目前不在 Puppeteer 中實現)
Puppeteer
使用 DevTools 協議 與瀏覽器進行通訊Browser
實例能夠擁有瀏覽器上下文BrowserContext
實例定義了一個瀏覽會話並可擁有多個頁面Page
至少有一個框架:主框架。 可能還有其餘框架由 iframe 或 框架標籤 建立frame
至少有一個執行上下文 - 默認的執行上下文 - 框架的 JavaScript 被執行。 一個框架可能有額外的與 擴展 關聯的執行上下文Worker
具備單一執行上下文,而且便於與 WebWorkers 進行交互API 很是豐富,看幾個經常使用的功能
這是 UI 自動化測試最經常使用的功能了,Puppeteer 的處理也至關簡單——使用選擇器
這兩個函數分別會在頁面內執行 document.querySelector
和 document.querySelectorAll
,但返回值卻不是 DOM 對象,如同 jQuery 的選擇器,返回的是通過本身包裝的 Promise,ElementHandle 封裝了經常使用的 click 、boundingBox 等方法
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();
// ...
});
複製代碼
經過 ElementHandle 並不能直接獲取對應 DOM 元素的屬性,須要使用專門的 API 操做
pageFunction 的代碼會在瀏覽器實例中執行,因此能夠用 Window 等 dom 對象;其返回值是整個方法的返回值
const searchValue = await page.$eval('#search', el => el.value);
const html = await page.$eval('.main-container', e => e.outerHTML);
const divsCounts = await page.$eval('div', divs => divs.length);
複製代碼
page.evaluate(pageFunction [, ...args]) 是上述方法的抽象,能夠在瀏覽器示例中執行任意方法
const result = await page.evaluate(x => {
return Promise.resolve(8 * x);
}, 7); // 7 會作爲實參傳入 pageFunction
console.log(result); // 輸出 "56"
複製代碼
前面提到的 ElementHandle 實例 能夠做爲參數傳給 page.evaluate
const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
複製代碼
Puppeteer 經過 page.keyboard
對象暴露操做鍵盤的接口
await page.keyboard.type('Hello World!', {delay: 100});
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!'
複製代碼
方法看名字就知道什麼意思,type 和 sendCharacter 做用很是相似,區別是
keypress
和 input
事件,不會觸發 keydown
或 keyup
事件keydown
, keypress
/input
和 keyup
事件特殊鍵名參考:github.com/puppeteer/p…
Puppeteer 經過 page.mouse
對象暴露操做鍵盤的接口
// 使用 ‘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();
複製代碼
手機頁面常用 tap 事件,用 page.mouse.click() 是不能觸發的,須要使用專門的 tap API
幾個頁面跳轉的 API 很是簡單,options 的 waitUntil
參數用來指定知足什麼條件認爲頁面跳轉完成,若是值爲事件數組,那麼全部事件觸發後才認爲是跳轉完成。事件包括:
load
- 頁面的load事件觸發時(默認值)domcontentloaded
- 頁面的 DOMContentLoaded 事件觸發時networkidle0
- 再也不有網絡鏈接時觸發(至少500毫秒後)networkidle2
- 只有2個網絡鏈接時觸發(至少500毫秒後)Puppeteer 提供了對一些頁面常見事件的監聽,用法和 jQuery 很相似
Puppeteer 提供了幾個有用的方法用來修改設備信息
await page.setViewport({
width: 1920,
height: 1080
});
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36');
複製代碼
page.emulateMedia(mediaType)
:能夠用來修改頁面訪問的媒體類型,但僅僅支持
page.emulate(options)
:前面介紹的幾個函數至關於這個函數的快捷方式,這個函數能夠設置多個內容
viewport
width
height
deviceScaleFactor
isMobile
hasTouch
isLandscape
userAgent
由於使用太頻繁,Puppeteer 經過 puppeteer/DeviceDescriptors
提供了全套的設備模擬
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone XR'];
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://www.google.com');
// other actions...
await browser.close();
});
複製代碼
全部支持參考:github.com/puppeteer/p…
經過 page.getMetrics() 能夠獲得一些頁面性能數據
{
Timestamp: 382305.912236,
Documents: 5,
Frames: 3,
JSEventListeners: 129,
Nodes: 8810,
LayoutCount: 38,
RecalcStyleCount: 56,
LayoutDuration: 0.596341000346001,
RecalcStyleDuration: 0.180430999898817,
ScriptDuration: 1.24401400075294,
TaskDuration: 2.21657899935963,
JSHeapUsedSize: 15430816,
JSHeapTotalSize: 23449600
}
複製代碼
page.exposeFunction(name, puppeteerFunction)
用於在 window 對象註冊一個函數,在自動化測試初始化測試環境時候頗有用,舉個例子:給 window 添加一個 window.readfile 函數
const puppeteer = require('puppeteer');
const fs = require('fs');
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
page.on('console', msg => console.log(msg.text));
// 註冊 window.readfile
await page.exposeFunction('readfile', async filePath => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, text) => {
if (err)
reject(err);
else
resolve(text);
});
});
});
await page.evaluate(async () => {
// use window.readfile to read contents of a file
const content = await window.readfile('/etc/hosts');
console.log(content);
});
await browser.close();
});
複製代碼
Puppeteer 默認運行 Chromium 的 headless mode。若是想要使用徹底版本的 Chromium 設置 'headless' option 便可。
const browser = await puppeteer.launch({headless: false});
複製代碼
默認狀況下,Puppeteer 下載並使用特定版本的 Chromium 以及其 API 保證開箱即用。 若是要將 Puppeteer 與不一樣版本的 Chrome 或 Chromium 一塊兒使用,在建立Browser
實例時傳入 Chromium 可執行文件的路徑便可:
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
複製代碼
const path = require('path');
const puppeteer = require('puppeteer');
const iPhoneXR = puppeteer.devices['iPhone XR'];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulate(iPhoneXR);
await page.goto('https://www.baidu.com', { waitUntil: ['load'] });
await page.screenshot({
path: path.join(__dirname, '../image', 'baidu.png'),
fullPage: true,
});
await browser.close();
})();
複製代碼
const path = require('path');
const fs = require('fs');
const http = require('http');
const https = require('https');
const puppeteer = require('puppeteer');
const ora = require('ora');
// const devices = require('puppeteer/DeviceDescriptors');
const iPhoneXR = puppeteer.devices['iPhone XR'];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulate(iPhoneXR);
await page.goto('https://iamge.baidu.com', { waitUntil: ['load'] });
await page.type('#image-search-input', 'dog');
await page.tap('#image-search-btn');
page.on('load', async () => {
const srcs = await page.$eval(
'.sfc-image-content-waterfall img',
images => images.map(img => img.src)
);
await browser.close();
let i = 0;
srcs.forEach(src => {
const request = src.trim().startsWith('https') ? https : http;
const dest = path.join(__dirname, `../images/${i++}.jpg`);
console.log(`正在下載 ${src}`);
request.get(src, res => {
res.pipe(fs.createWriteStream(dest));
});
});
});
})();
複製代碼