Skip to content

将node打包成可执行的二进制文件

https://github.com/vercel/pkg

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既可以全局安装也可以局部安装,推荐采用局部安装:

rust
npm install pkg --save-dev

使用

Shell
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-arm64node12-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. 以下是相对路径值的比较表格:

valuewith nodepackagedcomments
__filename/project/app.js/snapshot/project/app.js
__dirname/project/snapshot/project
process.cwd()/project/deploysuppose the app is called ...
process.execPath/usr/bin/nodejs/deploy/app-x64app-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.entrypointundefined/snapshot/project/app.js
process.pkg.defaultEntrypointundefined/snapshot/project/app.js
require.main.filename/project/app.js/snapshot/project/app.js

真实文件系统路径

获取可执行文件的真实文件系统路径(外部路径,存储可执行文件的目标设备路径)。在不同设备和环境下得到的路径支持情况不同

process.cwd()

valuewith nodepackaged
macos🚫
win--

process.execPath

valuewith nodepackaged
macos🚫
win-

推荐在本地 Node 环境执行时通过cross-env CURRENT=node node index.js, 采用process.cwd(). 在可执行文件执行时采用 process.execPath

JavaScript
// 真实文件系统目录
const realFileSystemDir = path.dirname(process.env.CURRENT === 'node' ? process.cwd() : process.execPath)
// 真实文件系统目录下所有文件
const currentDirFiles = fs.readdirSync(realFileSystemDir)

最佳实践

JSON
// 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"
  },
 }

img

img

扩展阅读

打包koa项目

项目代码打包

第一阶段的打包, 先把业务代码转成es5并压缩.

这里使用到了Rollup.

先安装依赖

Bash
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

创建 打包 配置文件

Shell
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();

配置命令

JSON
// /package.json
{
  "scripts": {
    "dev": "node ./src/app.js",
    "build": "node ./rollup-build.js"
  }
}

执行npm run build之后, dist文件夹就是打包出来的文件. 里面有业务代码和package.json.

dist文件夹上传到线上服务器, 并在服务器安装好node环境, 在dist中执行npm i, 安装依赖. 最后运行项目即可.

Bash
cd dist
npm i
node ./app.js

前端知识体系 · wcrane