以下为官方介绍文档(zhaoqize.github.io/puppeteer-a…
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。
能做什么?
你可以在浏览器中手动执行的绝大多数操作都可以使用 Puppeteer 来完成! 下面是一些示例:
- 生成页面 PDF。
- 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
- 自动提交表单,进行 UI 测试,键盘输入等。
- 创建一个时时更新的自动化测试环境。 使用最新的 JavaScript 和浏览器功能直接在最新版本的Chrome中执行测试。
- 捕获网站的 timeline trace,用来帮助分析性能问题。
- 测试浏览器扩展。
开始使用
安装
在项目中使用 Puppeteer:
npm i puppeteer
# or "yarn add puppeteer"
Note: 当你安装 Puppeteer 时,它会下载最新版本的Chromium(~170MB Mac,~282MB Linux,~280MB Win),以保证可以使用 API。 如果想要跳过下载,请阅读环境变量。
puppeteer-core
自 1.7.0 版本以来,我们都会发布一个 puppeteer-core 包,这个包默认不会下载 Chromium。
npm i puppeteer-core
# or "yarn add puppeteer-core"
puppeteer-core 是一个的轻量级的 Puppeteer 版本,用于启动现有浏览器安装或连接到远程安装。
具体见 puppeteer vs puppeteer-core.
使用
Note: Puppeteer 至少需要 Node v6.4.0,下面的示例使用 async / await,它们仅在 Node v7.6.0 或更高版本中被支持。
Puppeteer 使用起来和其他测试框架类似。你需要创建一个 Browser 实例,打开页面,然后使用 Puppeteer 的 API。
Example - 跳转到 example.com 并保存截图至 example.png:
文件为 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
Puppeteer 初始化的屏幕大小默认为 800px x 600px。但是这个尺寸可以通过 Page.setViewport() 设置。
Example - 创建一个 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
查看 Page.pdf() 了解跟多内容。
Example - 在页面中执行脚本
文件为 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
查看 Page.evaluate() 了解更多相关内容,该方法有点类似于 evaluateOnNewDocument and exposeFunction。
默认设置
1. 使用无头模式
Puppeteer 运行 Chromium 的headless mode。如果想要使用完全版本的 Chromium,设置 'headless' option 即可。
const browser = await puppeteer.launch({headless: false}); // default is true
2. 运行绑定的 Chromium 版本
默认情况下,Puppeteer 下载并使用特定版本的 Chromium 以及其 API 保证开箱即用。 如果要将 Puppeteer 与不同版本的 Chrome 或 Chromium 一起使用,在创建Browser实例时传入 Chromium 可执行文件的路径即可:
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); 具体见:Puppeteer.launch()
看这篇文章了解 Chromium 与 Chrome 的不同。这篇文章 介绍了一些 Linux 用户在使用上的区别。
3. 创建用户配置文件
Puppeteer 会创建自己的 Chromium 用户配置文件,它会在每次运行时清理。
资源 API Documentation Examples Community list of Puppeteer resources
下面是我使用过的具体实施方案(如有不足之处欢迎随时讨论)
1.项目主要依赖(npm 可直接安装):
- express ---node 的 常用http框架
- ioredis ---node 的 redis 连接工具
- isbot ---用于判断是否是爬虫的工具 (如果不是用于seo可不用)
- node-schedule ---node 的 定时任务插件(cron表达式)
- puppeteer ---无头浏览器 用于渲染html
- redlock ---redis 互斥锁,防止并发渲染导致服务器资源不足问题
- pm2 — node 多进程集群运行框架
2.具体思路
事前:
- 将打包好的项目文件放到项目 dist (任意一个名字)文件夹下。
- 如有条件,可以提前将所有页面使用 定时任务 提前渲染缓存(我请求我自己)。 事中:
- 用户请求一个页面的时候先去判断是否有已渲染过的缓存,如果有直接命中返回。
- 如果没有缓存先上全局锁,然后二次读缓存(排除并发时重复渲染问题),如无缓存,使用 puppeteer 进行渲染,渲染后,释放锁。
- 对于静态资源的请求,不经过ssr,直接命中返回(使用express方法)
- 对于一些跟页面展示无关的资源(比如微信sdk包、谷歌firebase 等等),可在 puppeteer 进行配置过滤不请求,关于 puppeteer API 可以查看文档使用 事后:
- 对于一些页面的更新可以使用定时任务进行更新,或者自行触发更新
3.具体参考代码
express + redis 判断层
var express = require('express');
var compression = require('compression')
var app = express();
var server = require('http').createServer(app);
var history = require('connect-history-api-fallback');
var isBot = require('isbot');
import SSR from '../utils/ssr'
import Config from '../Config'
import redisUtils from '../utils/RedisUtils'
import utils from '../utils/Utils'
import { updateSsrHtml, updateAllSsrHtml, refreshSiteMap } from '../utils/EventEmitter'
import { scheduleRefreshAllSSrHtml, scheduleRefreshSiteMap } from '../utils/schedule'
const Redlock = require('redlock');
const redlock = new Redlock([redis], {
retryDelay: 200, // time in ms
retryCount: 10,
});
var listenPort = Config.port;
const staticFileMiddleware = express.static('dist');
//初始化全局变量
global.updateAllSsrHtmlFlag = false
global.refreshSiteMapFlag = false
//本地测试
function isLocalHost(ua) {
return ua == "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1"
}
//尽量在其他中间件前使用compression
//gzip 压缩
app.use(compression());
app.use(function (req, res, next) {
var UA = req.headers['user-agent'];
//判断是否本身 无头浏览器请求
var is_puppeteer = false
if (UA.indexOf("HeadlessChrome/") != -1) {
is_puppeteer = true
}
let staticRescoure = ['static/', "manifest.json", "favicon.ico", 'img/', 'sitemap.xml', 'robots.txt', 'googlef58764e7fe61073e.html','umi.css','umi.js']
var isStaticDir = false
if (staticRescoure.find(regex => req.url.match(regex))) {
isStaticDir = true
}
let params = utils.getUrlParams(req.url)
if (!utils.isEmptyValue(params['updateAllSsrHtml']) && params['updateAllSsrHtml'] == "true" && !isStaticDir) {
updateAllSsrHtml()
}
if (!utils.isEmptyValue(params['refreshSiteMap']) && params['refreshSiteMap'] == "true" && !isStaticDir) {
refreshSiteMap()
}
// 判断是否是爬虫, 排除资源目录的请求
//if(UA && isBot(UA) && !isStaticDir){
if (UA && isBot(UA) && !isStaticDir && !is_puppeteer) {
// 生成本地访问链接
var requestUrl = Config.baseUrl + req.url;
(async () => {
let html = await redisUtils.get(req.url)
if (!utils.isEmptyValue(html)) {
console.log("获取缓存html成功: " + req.url)
res.send(html);
} else {
var resource = `locks:gethtml`;
// the maximum amount of time you want the resource locked,
// keeping in mind that you can extend the lock up until
// the point when it expires
var ttl = 1000;
redlock.lock(resource, ttl, async function (err, lock) {
// we failed to lock the resource
if (err) {
// ...
}
// we have the lock
else {
// ...do something here...
try {
let html = await redisUtils.get(req.url)
if (!utils.isEmptyValue(html)) {
console.log("获取缓存html成功: " + req.url)
res.send(html);
} else {
var results = await SSR(requestUrl);
redisUtils.set(req.url, results.html)
res.send(results.html);
}
} catch (e) {
console.log('ssr failed', e);
res.status(500).send('Server error');
}
// unlock your resource when you are done
lock.unlock(function (err) {
// we weren't able to reach redis; your lock will eventually
// expire, but you probably want to log this error
//console.error(err);
});
}
});
}
})();
return;
}
next();
});
// 静态资源直接返回
app.use(staticFileMiddleware);
// 如果资源没命中会继续、经过history rewirte后
app.use(history({
disableDotRule: true,
verbose: true
}));
// 再次处理
app.use(staticFileMiddleware);
server.listen(listenPort);
console.log("启动成功:" + listenPort)
SSR.js:
const puppeteer = require('puppeteer');
let browserWSEndpoint = null;
async function SSR(url){
let browser = null;
if(browserWSEndpoint){
console.log("browserWSEndpoint!")
try{
browser = await puppeteer.connect({browserWSEndpoint});
}catch(e){
// 可能失败
browserWSEndpoint = null;
browser = null;
}
}
if(!browserWSEndpoint){
browser = await puppeteer.launch({
headless: true,
ignoreHTTPSErrors: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--single-process', // <- this one doesn't works in Windows
'--disable-gpu'
],
devtools: false,//不自动打开控制台(浏览器显示时有效)
});
browserWSEndpoint = await browser.wsEndpoint();
}
const start = Date.now();
const page = await browser.newPage();
// 1. Intercept network requests.
await page.setRequestInterception(true);
page.on('request', async req => {
// 2. Ignore requests for resources that don't produce DOM
// (images, stylesheets, media).
const blacklist = ['google', 'firebase','/gtag/js', 'ga.js', 'analytics.js','jweixin-1.6.0.js','adsbygoogle','3gimg.qq.com'];
if (blacklist.find(regex => req.url().match(regex))) {
return req.abort();
}
const whitelist = ['document', 'script', 'xhr', 'fetch'];
if (!whitelist.includes(req.resourceType())) {
return req.abort();
}
req.continue();
});
try {
await page.goto(url,{
//await page.goto('你的spa链接',{
waitUntil: ['load', 'networkidle0'],//页面加载完并且500ms内没有请求发出判断页面渲染完毕
});
const html = await page.content();
await browser.close()
const ttRenderMs = Date.now() - start;
console.info(`Headless rendered page ${url}: ${ttRenderMs}ms`);
return {html,err:false};
}catch (err) {
console.error("请求链接:"+url+"错误");
return {html:'',err:true};
}
}
export default SSR;
定时任务参考:
const schedule = require('node-schedule');
import {updateAllSsrHtml,refreshSiteMap} from './EventEmitter'
import HttpUtils from './HttpUtils'
const scheduleRefreshAllSSrHtml = ()=>{
console.log("启动定时任务:scheduleRefreshAllSSrHtml")
// 2小时 执行任务
schedule.scheduleJob('0 0 0 1/1 * ?',()=>{
updateAllSsrHtml()
});
//updateAllSsrHtml()
}
const scheduleRefreshSiteMap = ()=>{
console.log("启动定时任务:scheduleRefreshSiteMap")
// 每天 执行一次任务
schedule.scheduleJob('0 0 0 1/1 * ?',()=>{
refreshSiteMap()
});
//refreshSiteMap()
}
module.exports = {
scheduleRefreshAllSSrHtml,
scheduleRefreshSiteMap
}