1. 执行 js 函数
  2. 调用外部函数
    1. page.addScriptTag
    2. page.exposeFunction
  3. 参考

puppeteer (2) 在页面环境下执行 Js 代码

执行 js 函数

page.evaluate(pageFunction[, ...args])

  • pageFunction 在页面环境中执行的函数
  • args 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
const puppeteer = require("puppeteer");
async function init() {
const browser = await puppeteer.launch({
headless: false,
args: ['--proxy-server="direct://"', "--proxy-bypass-list=*"],
});
const page = await browser.newPage();

await page.evaluate(async () => {
console.log("hello world");
});
}
init();

可以通过 page.evaluate 给页面环境中的函数传参
不过不能传 Function ,会变成 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
const args = [
"a",
1,
{ a: 1 },
[1, 2],
function() {
console.log("foo");
},
];
await page.evaluate(async (...args) => {
// 别的都正常,传进去的 function 变成了 undefined
console.log(...args); // 'a' 1 {a: 1} [1,2] undefined
}, ...args);

还可以传 ElementHandle 实例,然后在页面环境里直接操作对应的 dom

1
2
3
4
const bodyHandle = await page.$("body");
await page.evaluate(async body => {
console.log(body);
}, bodyHandle);

注意不能返回函数, 会变成空对象

1
2
3
4
5
6
const result = await page.evaluate(async (...args) => {
return function() {
console.log("foo");
};
});
console.log(result, typeof result); // {} 'object'

同样也不能返回 Dom

1
2
3
4
const result = await page.evaluate(async (...args) => {
return document.body;
});
console.log(result, typeof result); // {} 'object'

另外官方文档称,不能被序列化的返回值会变成 undefined.

调用外部函数

不能像这样在 evaluate 调用外部函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const puppeteer = require("puppeteer");

async function init() {
const browser = await puppeteer.launch({
headless: true,
args: ['--proxy-server="direct://"', "--proxy-bypass-list=*"],
});
const page = await browser.newPage();

function say() {
console.log("hello world");
}
page.evaluate(() => {
// 不能在 evaluate 直接调用外面的函数 say
say(); // ReferenceError: say is not defined
});
}

init();

也不能将函数当作参数传给 page.evaluate

1
2
3
4
page.evaluate(say => {
console.log(say); // undefined
say();
}, say);

最简单的解决办法就是把 function 写在 evalute 里

1
2
3
4
5
6
7
await page.evaluate(() => {
function say() {
console.log("hello world");
}
console.log(say);
say();
});

但是有时没法这么干,对于这种状况有两种解决办法:

page.addScriptTag

page.addScriptTag 会将 content 中的字符内容作为 script 添加到 head 中.

1
2
3
4
await page.addScriptTag({ content: `${say}` });
page.evaluate(() => {
say(); // hello world
});

注意,如果是箭头函数,别忘了加上函数名,否则会报错.

1
2
3
4
const say = () => {
console.log("hello world");
};
await page.addScriptTag({ content: `const say = ${say}` });

2.PNG

作为对比, 如果没有在 content 中 添加 "const say =" 的话会报错,效果如下

1
2
3
4
5
await page.addScriptTag({ content: `${say}` });
const say = () => {
console.log("hello world");
};
// ReferenceError: say is not defined

捕获.PNG

对于对象方法, 则貌似没有什么好的解决办法.因为对象方法转成字符串之后是没有 function 关键字. 即使在前头加上"const say =" 之类的字符,也无法作为方法来使用.
1.PNG

不要这样写

1
2
3
const obj = {
method() {}
}

替代的解决办法:

1
2
3
const obj = {
method: function() {}
}

page.exposeFunction

page.exposeFunction 会在 window 下绑定一个同名函数.在 evaluate 中调用同名函数时, 这个函数会在 node.js 环境下执行,并返回一个 Promise 对象.

1
2
3
4
5
6
7
8
9
async function say() {
console.log("hello world");
}
await page.exposeFunction("say", say);
await page.evaluate(() => {
console.log(say);
say();
});
// hello world

evaluate 中的 console.log(say) 输出的效果
1.PNG

绑定的函数会在 nodejs 环境下运行,然后将结果包在 Promise 中返回.
因此 hello world 输出在命令行里, 而不再 chromium 浏览器中.
1.PNG

也不能传递 Dom Function 之类无法序列化的对象作为参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function say(...args) {
console.log(args);
}
await page.exposeFunction("say", say);
await page.evaluate(() => {
say(
"a",
1,
[1, 2],
{ a: 1 },
document.body,
() => {
console.log("foo");
},
function() {
console.log("bar");
}
);
});

捕获.PNG

不要忘了在 page.exposeFunction 前加 await ,否则可能会报错.

1
2
3
4
5
6
7
// 这里忘了加 await
page.exposeFunction("say", say);
await page.evaluate(() => {
console.log(say);
say();
});
// UnhandledPromiseRejectionWarning: Error: Evaluation failed: Invalid arguments: should be exactly one string.

exposeFunction 只接受函数, 例如把 path 模块传进去,然后调用 path.join() 的话会报错.

1
2
3
4
5
6
7
const path = require("path");
await page.exposeFunction("path", path);
await page.evaluate(() => {
console.log(path, path.join);
path.join("a", "b");
});
// Error: Evaluation failed: TypeError: path.join is not a function

解决办法,用啥传啥

1
2
3
4
5
6
const path = require("path");
await page.exposeFunction("join", path.join);
await page.evaluate(async () => {
const p = await join("a", "b"); // 注意别忘了 await, 传回来的是个 Promise
console.log(p); // a\b
});

exposeFunction 函数实质上在外部的 Node 环境下运行, 因此可以使用 bind 绑定函数的 this/柯里化函数.(page.addScriptTag 不能这样作)

1
2
3
4
5
6
7
8
function outerFunc(a, b) {
console.log(a, b); // 1 2
return [a, b]
};
await page.exposeFunction('outFunc', outerFunc.bind(this, 1));
await page.evaluate(async () => {
outerFunc(2);
})

务必注意,网页内的 iframe 可能会导致 await page.exposeFunction 卡死

参考

官方文档 page.evaluate
javascript - How can I dynamically inject functions to evaluate using Puppeteer? - Stack Overflow