使用pkg打包puppeteer项目提示Passed function is not well-serializable报错的原因分析及解决方法

最近使用node写了一个使用Puppet的自动化程序,为了方便分发给未安装node环境的用户,准备使用pkg对项目进行打包。打包过程中遇到了 Error: Passed function is not well-serializable!的报错,在此记录下问题的解决方法。

原因分析

Passed function is not well-serializable! 这个报错既不是浏览器抛的,也不是 pkg 抛的,而是 Puppeteer 自己抛的,而这个问题恰好是下面两个原因叠加造成的。

Puppeteer传递回调函数的原理

page.evaluate(fn, ...args) 里的函数是在 Chromium 渲染进程(页面上下文) 里执行的,而JS代码跑在Node 进程里。两个进程之间只能通过 CDP(Chrome DevTools Protocol)传字符串,不能直接传函数对象。所以在 Puppeteer 的 ExecutionContext.js 里,序列化的核心就是下面这两步:

1
2
3
4
5
6
7
8
9
10
11
12
13
let functionText = pageFunction.toString();          // 1. 靠 toString 拿源码
try {
new Function('(' + functionText + ')'); // 2. 校验:能否重新解析成合法函数
} catch (e1) {
// 尝试加 "function "/"async function " 前缀再试一次(兼容箭头函数等)
...
try {
new Function('(' + functionText + ')');
} catch (e2) {
throw new Error('Passed function is not well-serializable!'); // 这里抛错
}
}
// 3. 通过 CDP Runtime.callFunctionOn 把这段源码字符串发到浏览器

这个功能完全依赖 fn.toString() 返回一段合法的 JS 源码。直接用 node 跑时没问题,因为源码就在磁盘上,.toString() 能还原出函数体。

Pkg打包的过程

pkg官方文档记录了以下内容:

Files specified as scripts will be compiled using v8::ScriptCompiler and placed into executable without sources.

By default, your source code is precompiled to v8 bytecode before being written to the output file.

这说明pkg 打包的过程是把项目里的 JS 文件,用 v8::ScriptCompiler 全部编译成V8 字节码,塞进一个snapshot虚拟文件系统里,原始源码并没有不写进 exe。源码被剥离之后,运行时你的函数是从字节码”重建”出来的。这时候再调用 fn.toString(),拿到的就不是原始代码了,而是一个类似function () { [bytecode] } 或者 function () { [native code] }这样的占位符,它显然没法在 new Function(...) 里被重新解析成合法函数。于是 Puppeteer 在序列化 page.evaluate(dados => {...}) 时会依次产生下面的后果:

  • .toString() 拿到的是占位符;
  • new Function(...) 校验失败;
  • 抛出 Passed function is not well-serializable!。

解决方法

添加 --public 参数

一个最简单的方法就是在使用pkg打包时加入 --public 参数,或根据实际选择合适的参数

参数 作用
--no-bytecode 完全关闭字节码,所有文件(含 node_modules)都按明文 JS 打包
--public 只把顶层项目源码按明文打包(node_modules 仍字节码)
--public-packages "pkgA" 强制指定 npm 包按明文打包
默认(--private 行为) 全部字节码,源码隐藏

其他方法

  • 改用 eval() 包一层:把代码写进字符串里用 eval 拼接,避免让 Puppeteer 对函数做 .toString()。
  • 直接给 page.evaluate 传字符串:Puppeteer 的 evaluate 也接受字符串参数,字符串不经过函数序列化。

参考链接

  1. Stack Overflow 原问题:Passed function is not well-serializable
  2. Puppeteer ExecutionContext.js(含 “not well-serializable” 校验逻辑)
  3. Puppeteer helper.js(evaluationString 字符串序列化)
  4. pkg README(--public / --no-bytecode / --public-packages 说明)
--- 本文结束  The End ---