使用 pkg 打包 ThinkJS 项目

在 ThinkJS 的用户群里,常常有开发者提出须要对源码进行加密保护的需求。咱们知道 JavaScript 是一门动态语言,不像其余静态语言能够编译成二进制包防止源码泄露。因此就出现了 pkgnexe 之类的工具,支持将 JS 代码连同 Node 一块打包成一个可执行文件,一来解决了环境依赖的问题,二来解决了你们关心的源码保护的问题。node

pkg 模块的 README 中,罗列了它的几大用处,若是你有下面的几个需求的话建议不妨试试。linux

  • 为应用提供商业发行版而不用暴露源码
  • 为应用提供 demo 而不用暴露源码
  • 一键打包全部平台可执行文件而不须要对应平台环境依赖
  • 提供自解压或自安装的解决方案
  • 运行应用不须要安装 Node.js 和 npm
  • 部署仅须要一份单文件,不须要经过 npm 安装大量的依赖
  • 资源打包后让应用迁移起来更加方便
  • 在指定 Node.js 版本下对应用进行测试而不须要安装对应的版本

如何使用

关于 pkg 模块的基础使用,你们能够看 《把你的NodeJS程序给没有NodeJS的人运行》 这篇文章。经过 npm install -g pkg 在全局安装上模块后就能够在命令行中使用 pkg 命令了。pkg 除了支持在命令行中指定参数以外,还支持在 package.json 中进行配置。git

{
  ...
  "bin": "production.js",
  "scripts": {
    "pkg": "pkg . --out-path=dist/"
  },
  "pkg": {
    "scripts": [...]
    "assets": [...],
    "targets": [...]
  },
  ...
}

以上就是一个简单的配置。bin 用来指定最终打包的入口文件,pkg.scriptspkg.assets 用来指定除了入口文件以外须要打包进可执行文件中的内容,其中前者用来指定其余 .js 文件,后者用来指定非.js的资源。pkg.targets 则是用来指定须要打包的平台,平台名称结构以下,node${version}-${platform}-${arch}version 用来指定具体 Node 的版本,platform 用来指定编译的平台,能够是 freebsd, linux, alpine, macos 或者 win,最后 arch 用来指定编译平台的架构,能够是 x64, x86, armv6 或者 armv7。例如 node10-macos-x64 表示的就是基于 Node 10 打包在 MacOS 平台上执行的可执行程序。scripts, assetstargets 都支持数组配置多个。github

将入口文件、依赖的脚本和资源、须要编译的平台配置好以后,执行 npm run pkg 便可完成编译。sql

如何打包 ThinkJS

pkg 的原理大概是提供一个虚拟的文件系统,将 __filename, __dirname 等变量以及官方 API 中的 IO 操做方法指向本地文件系统的变量修改为指向虚拟系统。经过该虚拟文件系统读取压缩打包后的程序源码,提供脚本执行的环境。须要注意的是该虚拟文件系统是只读的,因此若是程序中有基于 __dirname 进行读写操做的方法,须要规避规避掉。macos

代码预处理

在 ThinkJS 项目中会有如下两个地方有文件写入操做:npm

  1. 项目启动后会在 runtime/config/${env}.json 下写入最终的配置文件
  2. 生产环境下默认会在 logs/ 目录中写入线上日志

这些目录默认都是基于当前项目文件夹的,因此基于以前的理论都须要规避。pkgREADME 中告诉咱们 process.cwd() 仍是会指向到真实的环境中,因此咱们能够修改以上目录的位置到 process.cwd() 来解决这个问题。json

//pkg.js
const path = require('path');
const Application = require('thinkjs');

const instance = new Application({
  //在启动文件中能够自定义配置 runtime 目录
  RUNTIME_PATH: path.join(process.cwd(), 'runtime'), 
  ROOT_PATH: __dirname,
  proxy: true,
  env: 'pkg',
});

instance.run();

基于 production.js 咱们新建一个 pkg.js 启动文件,定义项目启动后的 RUNTIME_PATH 路径,并将 env 赋值为 pkg,方便后续的配置中经过 think.env === 'pkg' 来切换配置。数组

//src/config/adapter.js
const {Console, DateFile} = require('think-logger3');
const isDev = think.env === 'development';
const isPkg = think.env === 'pkg';
exports.logger = {
  type: isDev ? 'console' : 'dateFile',
  console: {
    handle: Console
  },
  dateFile: {
    handle: DateFile,
    level: 'ALL',
    absolute: true,
    pattern: '-yyyy-MM-dd',
    alwaysIncludePattern: true,
    filename: path.join(isPkg ? process.cwd() : think.ROOT_PATH, 'logs/app.log')
  }
};

在 adapter 配置中咱们将原来基于 think.ROOT_PATH 的路径修改为基于 process.cwd()。除了日志服务以外,若是业务中有使用到 cache 和 session 等服务,它们若是也是基于文件存储的话,也须要修改对应的文件存储配置。固然这些都是 ThinkJS 自带的一些服务,若是项目中有用到其它的一些服务,或者说自己的业务逻辑中有涉及到文件写入的也都须要修改配置。bash

打包配置

项目的写入操做规避掉以后咱们就能够正常的配置 pkg 而后进行打包处理了。一份简单的 pkg 模块的配置大概是这样的:

//package.json
{
  "bin": "pkg.js",
  "pkg": {
    "assets": [
      "src/**/*",
      "view/**/*",
      "www/**/*"
    ],
    "targets": [
      "node10-linux-x64",
      "node10-macos-x64",
      "node10-win-x64"
    ]
  }
}

这里咱们指定了 pkg.js 为打包的入口文件,指定了须要编译出 linux, macos, win 三个平台的可执行脚本,同时指定了须要将 src/, view/, www/ 三个目录做为资源一块打包进去。这是由于 ThinkJS 是动态 require 的项目,具体的业务逻辑都是在执行的时候经过遍历文件目录读取文件的形式载入的,对于 pkg 模块打包来讲没法在编译的时候知道这些依赖关系,因此须要做为启动依赖的“资源”一块打包进去。

配置好后直接在项目目录下执行 pkg .,若是一切 OK 的话应该能在当前目录中看到三个可执行文件,直接执行对应平台的二进制文件便可启动服务了。

➜  www.thinkjs.org git:(master) npm run pkg-build

> thinkjs-official@1.2.0 pkg-build /Users/lizheming/workspace/thinkjs/www.thinkjs.org
> pkg ./ --out-path=dist

> pkg@4.4.0
➜  www.thinkjs.org git:(master) ✗ ls -alh dist
total 577096
drwxr-xr-x   5 lizheming  staff   160B 12 28 17:35 .
drwxr-xr-x@ 30 lizheming  staff   960B 12 28 17:34 ..
-rwxr-xr-x   1 lizheming  staff    87M 12 28 17:34 thinkjs-official-linux
-rwxr-xr-x   1 lizheming  staff    87M 12 28 17:35 thinkjs-official-macos
-rw-r--r--   1 lizheming  staff    82M 12 28 17:35 thinkjs-official-win.exe
➜  www.thinkjs.org git:(master) ✗

后记

项目打包后有一个问题是配置没办法修改了,若是有动态配置的需求的话就不是很方便了。这里提供两个思路解决该问题:

  1. 将动态的配置配置到环境变量中,程序经过读取环境变量覆盖默认的配置。
  2. 利用 ThinkJS 提供的 beforeStartServer() 钩子在启动前读取真实目录下的配置文件进行配置覆盖。

    //pkg.js
    const path = require('path');
    think.beforeStartServer(() => {
      const configFile = path.join(process.cwd(), 'config.js');
      const config = require(configFile);
      think.config(config);
    });

另外随着项目的复杂度提升,业务内可能会引入大量的第三方模块。前文只是解决了 ThinkJS 项目自己的动态引入问题,若是引入的第三方模块也有动态引入的话也须要在 pkg.assets 配置中显示指定出来。还有就是针对 C++ 模块,pkg 目前尚未办法作到自动引入,一样须要在 pkg.assets 中指定依赖资源。

//package.json
{
  "pkg": {
    "assets": [
      //以 node-sqlite3 模块为例
      "node_modules/sqlite3/lib/binding/node-v64-darwin-x64/node_sqlite3.node"
    ]
  }
}

其中 node-v64-darwin-x64 可能会根据平台不同致使名字不太同样。没法引入 .node 模块的缘由是由于 C++ 模块安装的时候会经过 node-gyp 进行动态编译,该操做是和平台相关的。也就是说该特性和 pkg 模块在一个平台上能打包全部平台的二进制包特性是冲突的,毕竟 pkg 模块也没办法在 Mac 平台上编译 Windows 平台的模块。因此在这种状况下除了须要手动引入编译后的 .node 模块以外,还须要注意引入的该 .node 模块和 pkg.targets 指定的编译平台的一致性。

获取 .node 模块除了在对应平台模块安装以外,也能够选择下载其它同窗提供编译好的模块。淘宝源上提供了不少二进制模块的编译后结果,以 node-sqlite3 为例,它的全部编译模块能够在 https://npm.taobao.org/mirror... 这里下载,自行选择对应的版本和平台便可。

本文说的打包配置都已在 ThinkJS 官网 项目中实现,想要尝试的同窗能够直接克隆官网项目,安装完依赖后执行 npm run pkg-build 便可在 dist/ 目录中得到二进制可执行文件。

相关文章
相关标签/搜索