如何优雅的在 Puppeteer 环境中运行 JavaScript

有时我们可能需要在 Puppeteer 环境中执行一段 JS 代码。
根据官方提供的 API,我们有两种选择,一种是添加 script 标签的方式引入 JS。

page.addScriptTag(options)

  • options <[Object]>
  • url <[string]> URL of a script to be added.
  • path <[string]> Path to the JavaScript file to be injected into frame. If path is a relative path, then it is resolved relative to [current working directory].
  • content <[string]> Raw JavaScript content to be injected into frame.
  • type <[string]> Script type. Use ‘module’ in order to load a Javascript ES6 module. See script for more details.
  • returns: <[Promise]<[ElementHandle]>> which resolves to the added tag when the script’s onload fires or when the script content was injected into frame.

另一种是使用page.evaluate

1
2
3
4
const result = await page.evaluate(x => {
return Promise.resolve(8 * x);
}, 7);
console.log(result); // prints "56"
  • page.addScriptTag虽然可以引用本地文件作为 JS 执行,但是模块系统(ES6 Module 和 CommonJS 等)支持并不完善,部分 ES6 代码不支持 (最新 Chrome 可忽略)。
  • page.evaluate中的代码相当于在 DevTools 的控制台执行的,同样也有模块系统和 ES6 的问题,只是函数传值比page.addScriptTag方便。

所以我认为最好的解决方式是引入 webpack 和 babel 的编译机制。
具体方案是使用 Webpack Node API 配合 memory-fs 将要执行的 JS 文件作为入口,将编译结果输出为字符串,再通过page.evaluate执行。


首先我们需要使用 webpack、memory-fs 和 babel 模块,所以先安装相关依赖。
yarn add webpack memory-fs @babel/core @babel/preset-env babel-loader

接下来我们编写一个buildModule函数来将指定文件作为入口,将相关模块依赖打包为 JS Bundle 字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const webpack = require('webpack');
const MemoryFS = require('memory-fs');

const buildModule = file => {
const compiler = webpack({
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: require.resolve(file),
output: {
filename: 'bundle.js',
path: '/build',
},
module: {
rules: [
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
},
},
],
},
});
const fs = new MemoryFS();
compiler.outputFileSystem = fs;
return new Promise((resolve, reject) => {
compiler.run(error => {
if (error) {
reject(error);
return;
}
const content = fs.readFileSync('/build/bundle.js');
resolve(content.toString());
});
});
};

我们可以在根目录新建babel.config.js来指定打包时的 babel 配置,当然也可以复用项目已有的配置。

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
presets: [
[
'@babel/preset-env',
{
modules: false,
useBuiltIns: 'entry',
corejs: 3,
targets: {
chrome: 70,
},
},
],
],
};

最后,我们准备需要执行的 JS 入口文件,如果需要执行某些函数,可以将相关模块暴露到 window 对象,供 Puppeteer 使用。在这里我们将自己写的 sum 函数暴露到 window 上。

browser_run.js

1
2
3
4
5
import add from 'lodash/add';

const sum = (...param) => [...param].reduce((a, b) => add(a, b), 0);

window.sum = sum;

最终在 Puppeteer 中调用buildModule函数,传入入口文件路径,经由 webpack 打包和 babel 编译,最后通过page.evaluate函数执行。

1
2
3
4
5
6
7
8
9
10
11
const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const scriptStr = await buildModule('./browser_run.js');
await page.evaluate(scriptStr);
await page.evaluate(() => {
console.log('sum:', window.sum(1, 2, 3, 4, 5));
});
})();
0%