使用 pkg 把依赖 puppeteer 的 nodejs 脚本打包成可执行文件 踩坑记录

3,000 阅读4分钟

前言

写了一个 nodejs 脚本,需要给到公司后端使用,但是有些后端同事并没有安装 nodejs,于是想到将脚本打包成 exe,这样即使在没有安装 nodejs 的电脑上也能正常运行

脚本使用到了 puppeteer (一个能够控制 Chromium 的库,可以做一些 如 爬虫、自动提交表单、自动化测试 等等操作,总之平时需要手动控制浏览器完成的操作都可以通过这个库自动完成)

在使用 pkg 把这个 nodejs脚本 打包成 可执行文件 时,遇到了许多坑

  • 一是因为 pkg 本身坑就多
  • 二是 puppeteer 相当于集成了一个 chrome ,然而 pkg 无法打包这个浏览器,所以又踩了另一个坑

pkg

pkg 可以将 nodejs 项目打包成 windows mac linux 的可执行文件,而且可以在未安装 nodejs 的设备上运行

使用起来很简单,只需安装 npm i -g pkg,然后打包 js 文件 pkg index.js 即可,使用简单但是坑不少

下面记录下踩坑的过程 和 解决方案

缓存中找不到二进制文件

$ pkg index.js
> pkg@5.7.0 
> Targets not specified. Assuming: node16-linux-x64, node16-macos-x64, node16-win-x64 
> Fetching base Node.js binaries to PKG_CACHE_PATH fetched-v16.15.0-linux-x64 [ ] 0%> Not found in remote cache: {"tag":"v3.4","name":"node-v16.15.0-linux-x64"} 
> Building base binary from source: built-v16.15.0-linux-x64 
> Error! Not able to build for 'linux' here, only for 'win'

缓存中找不到 node16-linux-x64, node16-macos-x64, node16-win-x64 估计网络不好,也没有成功下载下来

  1. 手动到 pkg-fetch 下载这三个文件(下载的版本看报错信息,我这里是 16.15.0 )
  2. 把下载下来的文件前缀改成 fetched, 得到这三个文件: fetched-v16.15.0-linux-x64, fetched-v16.15.0-macos-x64, fetched-v16.15.0-win-x64
  3. c:/users/${user}/.pkg-cache 下 ,新建一个 v3.4 文件夹(这个命名看报错信息的 tag ,我这里是 v3.4)
  4. 把三个文件放到刚刚建立的 v3.4 文件夹下
  5. 重新打包

闪退

手动下载二进制文件后可以打包了,但是打包出来的 exe 运行时闪退,而且什么报错信息都看不到,谷歌搜索一番,发现用 cmd 来执行,可以看到报错信息,于是 cmd 进入到文件所在目录,执行index.exe,可以看到报错信息了!

缺少 bin 字段

能看到报错信息就好办了

Error! Property 'bin' does not exist in [path to package.json]

根据报错,在 package.json 中加入 "bin": "index.js"

又解决一个bug,但是还没完😂!!

assets 配置

加入 bin 后,再次执行,发现还是报错

Error: File or directory 'C:\**\xxx\xxx\xxx\xxx.json' 
was not included into executable at compilation stage. Please recompile adding it as asset or script.

大概意思是项目中读取的一个 json 文件未加入到可执行文件中,需要添加到 asset 或 script 中

于是继续 google ,发现一个相关 issue:Not included into executable at compilation stage. Please recompile adding it as asset or script. ,看到大伙的解决方案是在 package.json 中配置 pkg.assets ,于是我把那个 json 加进来

// package.json 配置 pkg 字段
  "pkg": {
    "assets": [
      "res/xxx.json"
    ]
  }

重新打包,发现还是报错,相对路径绝对路径都试过,还是不行,于是去看文档 assets 部分,发现了这句话

image.png

原来要打包命令要改成 pkg package.json 或者 pkg .才能使用上 package.json 的配置

于是执行pkg package.json

又解决一个bug,但是还没完😂!!

无法 write 写入文件

Error: Cannot write to packaged file

继续搜索发现一个相关 issue :Cannot write to packaged file

image.png

原来路径中不能使用 __dirname ,应该用 process.cwd() 代替,原因也很简单,__dirname 指向 js 所在目录,process.cwd() 指向命令执行目录,打包后,可执行文件不一定与 js 所在目录相同,所以用 process.cwd()是合理的

于是修改写入路径

又解决一个bug,但是还没完😂!!

找不到 puppeteer 集成的 chrome

pkg 无法打包 puppeteer 集成的 chrome

Error: Could not find expected browser (chrome) locally. 
Run `npm install` to download the correct Chromium revision (991974).

一开始在找打包工具时,就发现有人遇到这个问题

image.png

于是继续 google 大法,在 stackoverflow 上发现一个相关问题 How do you package a puppeteer app?

image.png 大概方法是把 puppeteer 的 chrome 抽出来,再配置 executablePath

但是我试了下不行也不知道是什么原因,但是思路大概是这个思路

于是继续搜索,发现一个相关 issue:Puppeteer support?

看到有另一种写法

image.png

而且发现有个老兄根据这种写法专门开了个仓库 pkg-puppeteer

于是克隆这个仓库,跑了一下,发现可以成功启动 puppeteer 的 chrome,于是我也改成了他的写法

const puppeteer = require('puppeteer');
const isPkg = typeof process.pkg !== 'undefined';

//mac path replace
let chromiumExecutablePath = (isPkg ?
  puppeteer.executablePath().replace(
    /^.*?\/node_modules\/puppeteer\/\.local-chromium/,
    path.join(path.dirname(process.execPath), 'chromium')
  ) :
  puppeteer.executablePath()
);

console.log(process.platform)
//check win32
if (process.platform == 'win32') {
  chromiumExecutablePath = (isPkg ?
    puppeteer.executablePath().replace(
      /^.*?\\node_modules\\puppeteer\\\.local-chromium/,
      path.join(path.dirname(process.execPath), 'chromium')
    ) :
    puppeteer.executablePath()
  );
}
(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    executablePath: chromiumExecutablePath
  })
})()

进入 node_modules\puppeteer\.local-chromiumwin64-991974 拷贝到 build\chromium

然后执行 pkg package.json --out-path ./build ,将会把可执行文件打包到 build 文件夹中(记得把脚本使用到的路径也放进 build 中,例如我脚本中读取了 res 里面的文件,那么我也要把 res 文件夹放进 build 中)

最后进入 build 执行 exe ,成功!!!

至此,这个脚本打包成功了!!!