前言

背景

最近做了一个 Egg.js 的项目,有为客户私有化部署但是需要保护源代码的需求。之前没有接触过 Node.js 项目打包的,只是用过 Webpack 打包的前端的东西,更别说后端的项目打包了。搜索了一下,发现有不少方案,但是都只是解决了其中一部分问题,比如代码混淆。最后发现一个叫做 Pkg的方案应该能够满足需求。

目标

基础目标

  • 将 Egg.js 项目打包成不包含源文件的可执行文件

进阶目标

  • 确保包含了项目中所有依赖的文件
  • 确保打包结果不包含源代码
  • 确保结果不会被轻松反编译

安装

$ npm i -g pkg

简单使用

命令行

可以先用帮助看一下 pkg 的用法和参数,并不是很复杂。

$ pkg --help
  pkg [options] <input>

  Options:

    -h, --help       output usage information
    -v, --version    output pkg version
    -t, --targets    comma-separated list of targets (see examples)
    -c, --config     package.json or any json file with top-level config
    --options        bake v8 options into executable to run with them on
    -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

  Examples:

  – Makes executables for Linux, macOS and Windows
    $ pkg index.js
  – Takes package.json from cwd and follows 'bin' entry
    $ pkg .
  – Makes executable for particular target machine
    $ pkg -t node6-alpine-x64 index.js
  – Makes executables for target machines of your choice
    $ pkg -t node4-linux,node6-linux,node6-win index.js
  – Bakes '--expose-gc' into executable
    $ pkg --options expose-gc index.js

最简单的打包命令如下。

$ pkg index.js

使用以上命令即可完成对入口文件为 index.js 项目的打包,打包程序会自动遍历 js 文件中引用的其他文件并打到包内。完成后会分别得到 Mac、Linux 和 Windows 三个平台的可执行文件。

打包脚本

如果经常使用打包命令,最好还是在 package.json 文件里面添加一条打包命令。

{
  "name": "pkg",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "pkg . -t node12-linux-x64 -o server -d"
  },
  "bin": "./index.js"
}

然后直接运行下面的命令即可打包。

$ yarn build
$ npm run build // 或者

参数说明如下:

  • --target-t 表示目标 Node 版本、操作系统平台和架构
  • --output-o 表示得到的可执行文件名
  • --debug-d 表示调试模式,将所有调试信息都在控制台打印出来

Egg 项目下使用

Egg 项目有些特殊的情况,因此需要特殊处理。

具体的特殊情况如下所示:

  • 没有入口文件
  • run 目录运行时要写入文件
  • public 目录存放静态文件
  • 其他文件

没有入口文件

在根目录下创建一个 entry.js 文件。

// entry.js
'use strict';

require(__dirname + '/node_modules/egg-scripts/bin/egg-scripts');

package.json

{
    "bin": "entry.js"
}

光是这些还不够,因为入口文件并没有实际引用项目的文件,还需要在配置中直接指出要打包的项目文件。

package.json

{
    "bin": "entry.js"
    "pkg": {
        "scripts": [
            "app.js",
            "app/**/*.js",
            "config/*.js"
        ]
    }
}

Pkg 的文档中指出有两种方式可以手动指定要打包的文件,一种是 assets,另一种是 scripts,这里我们使用 scripts 来指定,因为 assets 主要是存放静态文件的,在打包的过程中不会做任何编译而直接把文件打进来,这样的话稍微一反编译文件就全都出来了,现在我们指定的文件正是我们便携的源代码文件,如果这样打包就失去了为了防止源代码泄露的意义了;而 scripts 表示的是要转成 V8 bytecode 的文件,并且打包工具还对 Node 运行时做了修补,告诉 V8 引擎在编译成 bytecode 时不保留源代码。

run 目录运行时要写入文件

config.default.js 中配置运行目录的地址,这样可以在实际运行的时候在可执行文件的目录下创建一个 run 目录。

config.rundir = process.cwd() + '/run';

public 目录存放静态文件

因为当前的项目没有使用静态文件,因此直接就关闭 static 插件就好了。

// config/plugin.js

static: {
    enable: false,
},

其他文件

目前还存在一些其他文件的问题,比如

  • app 目录下的 .graphql 定义文件
  • node_modules 下的文件
  • app 目录下的安装的插件自带 node_modules 目录

首先是前两种情况文件没有被包含进来,而第三种情况则是被手动包含进来以后要编译成 V8 的 bytecode,而有些 ES6 的原文件不能被当前的运行时所识别直接报错。

因此需要在 scripts 中排除所有 node_modules 下的文件,并在 assets 中包含进来。注意这里用的是 glob 的语法,需要将要排除的 pattern 放在最后面才能生效。

"bin": "entry.js",
"pkg": {
    "scripts": [
          "app.js",
          "app/**/*.js",
          "config/*.js",
          "!**/node_modules"
    ],
    "assets": [
          "app/**/*.json",
          "app/**/*.graphql",
          "**/node_modules/**/*"
    ]
}

然后在打包的时候输入以下命令就可以了。

$ yarn build
$ npm run build // 或者

在使用可执行文件的时候输入以下命令。

$ ./server start /snapshot/<project-name> --daemon --title=<egg-application-title>
  • <project-name> 表示当前项目所在的目录名称
  • <egg-application-title> 表示应用名称

参考资料