将node打包成可执行的二进制文件
Pkg是什么
Pkg可将Node.js项目打包为一个单独的可执行文件,可在未安装Nodejs的机器上运行。支持win、linux等多系统。
为什么使用pkg
Node.js应用不需要经过编译过程,可以直接把源代码拷贝到部署机上执行,确实比C++、Java这类编译型应用部署方便。然而,Node.js应用执行需要有运行环境,意味着你需要先在部署机器上安装Node.js。虽说没有麻烦到哪里去,但毕竟多了一个步骤,特别是对于离线环境下的部署机,麻烦程度还要上升一级。假设你用Node.js写一些小的桌面级工具软件,部署到客户机上还要先安装Node.js,有点“大炮打蚊子”的感觉。更严重的是,如果部署机器上游多个Node.js应用,而且这些应用要依赖于不同的Node.js版本,那就更难部署了。
理想的情况是将Node.js打包为一个单独的可执行文件,部署的时候直接拷贝过去就行了。除了部署方便外,因为不需要再拷贝源代码了,还有利于保护知识产权。
当然打包也可能被破解的,如果打包前将nodejs源码进行混淆加密,那就十分安全了。nodejs代码加密,可以用JShaman,简单方便,非常不错。
将Node.js打包为可执行文件的工具有pkg、nexe、node-packer、enclose等,从打包速度、使用便捷程度、功能完整性来说,pkg是最优秀的。这篇文章就来讲一讲半年来我使用pkg打包Node.js应用的一些经验。
pkg的打包原理简单来说,就是将js代码以及相关的资源文件打包到可执行文件中,然后劫持fs里面的一些函数,使它能够读到可执行文件中的代码和资源文件。例如,原来的require('./a.js')会被劫持到一个虚拟目录require('/snapshot/a.js')。
安装
pkg既可以全局安装也可以局部安装,推荐采用局部安装:
npm install pkg --save-dev
使用
pkg [options] <input>
Options:
-h, --help output usage information
输出使用帮助信息
-v, --version output pkg version
输出 pkg 版本信息
-t, --targets comma-separated list of targets (see examples)
以逗号分隔的目标列表(参考示例)
-c, --config package.json or any json file with top-level config
package.json 或者任何 json 文件顶层配置
--options bake v8 options into executable to run with them on
将 v8 选项打包到可执行文件中,以便它们一起运行
-o, --output output file name or template for several files
输出文件名或者多个文件的输出模板
--out-path path to save output one or more executables
保存输出可执行文件的路径
-d, --debug show more information during packaging process [off]
在打包过程中展示更多信息,默认关闭
-b, --build don't download prebuilt base binaries, build them
不下载预构建的基础二进制文件,而是构建它们
--public speed up and disclose the sources of top-level project
加速和公开顶级项目的源代码
--public-packages force specified packages to be considered public
强制指定包被认定为公开的
--no-bytecode skip bytecode generation and include source files as plain js
跳过字节码生成阶段,直接打包源文件为普通 js
--no-native-build skip native addons build
跳过原生插件构建
--no-dict comma-separated list of packages names to ignore dictionaries. Use --no-dict * to disable all dictionaries
以逗号分隔的包名列表忽略字典,使用 --no-dict * 禁用所有字典
-C, --compress [default=None] compression algorithm = Brotli or GZip
压缩算法 Brotli 或者 GZip. 默认关闭
Examples:
– Makes executables for Linux, macOS and Windows 打包 Linux, macOS 或者 Windows 的可执行文件
$ pkg index.js
– Takes package.json from cwd and follows 'bin' entry 通过当前目录下的 package.json 配置的 bin 入口打包
$ pkg .
– Makes executable for particular target machine 指定目标设备
$ pkg -t node14-win-arm64 index.js
– Makes executables for target machines of your choice 指定多个设备和 node 版本
$ pkg -t node12-linux,node14-linux,node14-win index.js
– Bakes '--expose-gc' and '--max-heap-size=34' into executable
$ pkg --options "expose-gc,max-heap-size=34" index.js
– Consider packageA and packageB to be public
$ pkg --public-packages "packageA,packageB" index.js
– Consider all packages to be public
$ pkg --public-packages "*" index.js
– Bakes '--expose-gc' into executable
$ pkg --options expose-gc index.js
– reduce size of the data packed inside the executable with GZip 通过 GZip 算法压缩可执行文件中的数据包大小
$ pkg --compress GZip index.js
Targets 目标
pkg 可以同时生成多个目标设备的可执行文件。你可以通过--targets
选项并以逗号分隔的列表来指定。一个规范的目标由以中横线连接的 3 部分组成,例如node16-macos-arm64
、 node12-macos-x64
或者node14-linux-arm64
:
- node版本 (node8), node10, node12, node14, node16 or latest
- 平台 alpine, linux, linuxstatic, win, macos, (freebsd)
- 架构 x64, arm64, (armv6, armv7)
已打包应用的使用方法
已打包应用当前目录下执行命令./app a b
等于node app.js a b
快照文件系统
打包过程中pkg
把项目文件打包到可执行文件中。这就叫做快照。在运行时已打包的应用程序有权限获取快照文件系统中存在的文件
已打包的文件路径中有/snapshot/
前缀(或者在Windows中C:\snapshot\
)。如果使用pkg /path/app.js
命令,然后__filename
的值在运行时将是/snapshot/path/app.js
. __dirname
的值将是/snapshot/path
. 以下是相对路径值的比较表格:
value | with node | packaged | comments |
---|---|---|---|
__filename | /project/app.js | /snapshot/project/app.js | |
__dirname | /project | /snapshot/project | |
process.cwd() | /project | /deploy | suppose the app is called ... |
process.execPath | /usr/bin/nodejs | /deploy/app-x64 | app-x64 and run in /deploy |
process.argv[0] | /usr/bin/nodejs | /deploy/app-x64 | |
process.argv[1] | /project/app.js | /snapshot/project/app.js | |
process.pkg.entrypoint | undefined | /snapshot/project/app.js | |
process.pkg.defaultEntrypoint | undefined | /snapshot/project/app.js | |
require.main.filename | /project/app.js | /snapshot/project/app.js |
真实文件系统路径
获取可执行文件的真实文件系统路径(外部路径,存储可执行文件的目标设备路径)。在不同设备和环境下得到的路径支持情况不同
process.cwd()
value | with node | packaged |
---|---|---|
macos | ✅ | 🚫 |
win | - | - |
process.execPath
value | with node | packaged |
---|---|---|
macos | 🚫 | ✅ |
win | - | ✅ |
推荐在本地 Node 环境执行时通过cross-env CURRENT=node node index.js
, 采用process.cwd()
. 在可执行文件执行时采用 process.execPath
// 真实文件系统目录
const realFileSystemDir = path.dirname(process.env.CURRENT === 'node' ? process.cwd() : process.execPath)
// 真实文件系统目录下所有文件
const currentDirFiles = fs.readdirSync(realFileSystemDir)
最佳实践
// packages.json
{
"name": "server-aarch64-apple-darwin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "node ./rollup-build.js",
"dev": "nodemon ./index.js",
"start": "node ./index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"pkg": "rm -rf package && pkg ."
},
// 配置打包入口
"bin": "dist/index.js",
// pkg 相关配置信息
"pkg": {
"targets": [
"node16-macos-arm64"
],
"outputName": "server",
"assets": ["./server.pid", "./dist/**"],
"outputPath": "package"
},
}
扩展阅读
打包koa项目
项目代码打包
第一阶段的打包, 先把业务代码转成
es5
并压缩.
这里使用到了Rollup.
先安装依赖
npm i rollup rollup-plugin-delete rollup-plugin-terser @babel/core @babel/plugin-transform-runtime@babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-json --save-devnpm i @babel/runtime --save
创建 打包 配置文件
touch rollup-build.js
// /rollup-build.js
const fs = require("fs");
const rollup = require("rollup");
const { babel, getBabelOutputPlugin } = require("@rollup/plugin-babel");
const del = require("rollup-plugin-delete");
const json = require("@rollup/plugin-json");
const commonjs = require("@rollup/plugin-commonjs");
const { terser } = require("rollup-plugin-terser");
// 获取根目录的'package.json'
const packageJSON = require("./package.json");
// 读取 生成模式 下需要的依赖包
const packageJSONForProduction = {
name: packageJSON.name,
dependencies: packageJSON.dependencies,
};
const inputOptions = {
input: "./index.js",
"includeNodeModules": true,
plugins: [
// 打包前先清空输出文件夹
del({ targets: "./dist/*" }),
// babel 相关的配置, 主要是做兼容
getBabelOutputPlugin({
presets: [["@babel/preset-env", { targets: { node: "current" } }]],
plugins: [["@babel/plugin-transform-runtime", { useESModules: false }]],
}),
babel({ babelHelpers: "bundled", exclude: "node_modules/**" }),
// 这里是把入口文件(app.js)以外的业务代码也进行打包(require进来的文件)
json(),
commonjs(),
// 代码的压缩或混淆
terser(),
],
};
const outputOptions = { dir: "./dist", format: "cjs" };
async function build() {
// create a bundle
const bundle = await rollup.rollup(inputOptions);
// generate code and a sourcemap
// const { code, map } = await bundle.generate(outputOptions);
// or write the bundle to disk
await bundle.write(outputOptions);
// 生成生产模式的 package.json, 在服务器上使用
const writeStream = fs.createWriteStream("./dist/package.json");
writeStream.write(JSON.stringify(packageJSONForProduction));
}
build();
配置命令
// /package.json
{
"scripts": {
"dev": "node ./src/app.js",
"build": "node ./rollup-build.js"
}
}
执行npm run build
之后, dist
文件夹就是打包出来的文件. 里面有业务代码和package.json
.
把dist
文件夹上传到线上服务器, 并在服务器安装好node
环境, 在dist
中执行npm i
, 安装依赖. 最后运行项目即可.
cd dist
npm i
node ./app.js