实战Puppeteer-使用原生代码与页面交互
概述
我们前面已经介绍不少Puppeteer实现操控页面对象的例子。现在我们应该停下来,好好思考一下,Puppeteer实现的原理是什么?
我们不需要通过Puppeteer源码来理解Puppeteer,只需要通过Puppeteer提供的API,我们透过这些API来探索性分析其实现机理。
原理初探
Puppeteer提供的对象模型中,最核心的对象就是page对象和EventHandler对象。我们仔细思考下,page对象可以看做是整个网页在浏览器中构建出的dom树一个包装对象,因此page对象有点类似JavaScript中的document,而EventHandler则是整个dom树按照某种筛选条件形成的dom节点包装对象。
我们通过page对象和EventHandler对象提供的方法也可以感知其对象模型设计的基本背景。
而从我们编写代码角度,我们要基于这些对象编程,就需要熟记这些对象模型提供的方法。我们对原生的网页编程技术更加熟悉的话,Puppeteer提供一种机制,让我们直接进入到原生的编程模式。
page对象和EventHandler对象都提供了通过原生编程的模式支持。
那么Puppeteer为何会提供这样的机制出来?我的理解可能是这样的:
- 我们大多数人对原生的网页编程非常熟悉,如果能够直接使用原生编程,对于这些开发者而言,就显得如鱼得水。
- 既然page和eventhandler都是对dom数据的对象包装,包装出来的对象提供的各种能力可能就存在欠缺,索性就开放一个机制,让开发者直接在原生态下进行操作dom树。这样既可以满足开发者需求,又可能确保puppeteer框架稳定及演进。
page原生编程
page提供一个方法evaluate,允许开发者直接进入整个网页中,采取原生的js编程技术,对整个网页的dom对象进行灵活的操作。
下面我们先给出一个简单的例子,来说明下这个方法的基本使用:
let page = await browser.newPage()
await page.goto("http://www.xxx.com")
let r = await page.evaluate(()=>{
//在这个方法体内,实际上就进入该网页的<script></script>环境中,我们可以对整个网页进行操作
//如我们对网页背景色进行设置
document.body.backgroundColor = "red"
})
我们可以把page提供的这个方法看做是一个门,进入这个门后,我们就可以按照传统的原生js编程技术,对整个网页的dom树进行操作,这个就和我们传统的网页编程一样了。
当然,有一点点区别还是存在的。
- 第一个就是这个方法体可以有返回值,如果没有显式返回值,就认为返回值为null,如上述代码中,evaluate执行完成后,r=null
- evaluate参数是一个函数,这个函数可以是传统的函数定义,也可以采取闭包模式。闭包好处是可以直接引入外部变量,不用显式提供函数参数值,如果是传统的函数定义,则需要传递函数参数值。我们通过如下两段代码来说明:
let page = await browser.newPage()
await page.goto("http://www.xxx.com")
let myColor = "red"
let r = await page.evaluate(()=>{
//在这个方法体内,实际上就进入该网页的<script></script>环境中,我们可以对整个网页进行操作
//如我们对网页背景色进行设置
document.body.backgroundColor = myColor //这里引用外部变量
})
上述代码说明闭包函数定义的优势地方。
let page = await browser.newPage()
await page.goto("http://www.xxx.com")
let r = await page.evaluate((myColor)=>{
//在这个方法体内,实际上就进入该网页的<script></script>环境中,我们可以对整个网页进行操作
//如我们对网页背景色进行设置
document.body.backgroundColor = myColor //这里是函数参数变量
}, ‘red’) //注意这里的'red'就是前面定义的函数参数值,需要传递过去的。
当我们进入到原生的网页内部,我们可以利用js+css技术,对网页进行任意的操作,我们就不展开了,就举一个场景:一般网页打开后,会很长,尤其是新闻类网站,可能我们需要进行滚动,那么我们就可能利用window提供的scroll方法,操作网页进行滚动。
page还提供与此相关的几个方法,大家可以参考其官方文档,并利用自己搭建的学习环境来研究下。
EventHandler原生编程
与Page主要的差别是EventHandler是整个网页的dom子树,而后者是整个dom树。同样,EventHandler也提供机制让我们进入到这个dom子树,采取原生网页技术进行操作这个dom子树。
EventHandler主要使用$eval或$eval让开发者进入到原生编程模式。这两个方法page对象也提供的,
我们通过一个例子来说明一下,假设一个网页的dom结构如下:
<html>
<head></head>
<body>
<form id="order">
<input name="name" />
<input name="age" />
<input name="sex" />
</form>
</body>
</heml>
先给出一个$eval例子
let eh = await page.$("#order") //eh就是一个EventHandler
await eh.$eval("input[name=name]",node=>{
node.value = "小明"
})
我们前面介绍到,$选择器,返回一个满足此查询条件的dom节点,上述$eval第一个参数就是css选择器,将会选择第一个input节点,作为dom节点对象,作为$eval函数的第二个参数,也是一个函数的参数值,也就是下面函数的参数node的值进行传递:
node => {
node.value = "小明"
}
$eval返回的是一个数组。我们还是从一个例子出发,来说明下:
let eh = await page.$("#order") //eh就是一个EventHandler
await eh.$eval("input",nodes=>{
nodes.map(node=>node.value = "小明")
})
上述dom子树中,有三个input节点。它们作为一个数组,传递给后续的那个闭包参数nodes。通过map操作,把所有的输入框中都输入“小明”。
最终的dom树是这样的:
<html>
<head></head>
<body>
<form id="order">
<input name="name" value="小明"/>
<input name="age" value="小明"/>
<input name="sex" value="小明"/>
</form>
</body>
</heml>
小结
page和eventhandler提供的这种原生编程模式,为我们操作dom打开自由之门,大家可以多做点小测验,加深理解这样原生操作模式能够带给我们怎样的好处。