使用pkg打包ThinkJS项目的方法步骤


Posted in Javascript onDecember 30, 2019

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

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

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

如何使用

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

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

以上就是一个简单的配置。bin 用来指定最终打包的入口文件,pkg.scripts 和 pkg.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, assets 和 targets 都支持数组配置多个。

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

如何打包 ThinkJS

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

代码预处理

在 ThinkJS 项目中会有以下两个地方有文件写入操作:

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

这些目录默认都是基于当前项目文件夹的,所以基于之前的理论都需要规避。pkg 的 README 中告诉我们 process.cwd() 还是会指向到真实的环境中,所以我们可以修改以上目录的位置到 process.cwd() 来解决这个问题。

//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 自带的一些服务,如果项目中有用到其它的一些服务,或者说本身的业务逻辑中有涉及到文件写入的也都需要修改配置。

打包配置

项目的写入操作规避掉之后我们就可以正常的配置 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/mirrors/sqlite3这里下载,自行选择对应的版本和平台即可。

本文说的打包配置都已在 ThinkJS 官网 项目中实现,想要尝试的同学可以直接克隆官网项目,安装完依赖后执行 npm run pkg-build 即可在 dist/ 目录中获得二进制可执行文件。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript While 循环基础教程
Apr 05 Javascript
重载toString实现JS HashMap分析
Mar 13 Javascript
Javascript模块化编程(一)AMD规范(规范使用模块)
Jan 17 Javascript
文本框(input)获取焦点(onfocus)时样式改变的示例代码
Jan 10 Javascript
jQuery中unwrap()方法用法实例
Jan 16 Javascript
JavaScript性能优化之小知识总结
Nov 20 Javascript
js数字计算 误差问题的快速解决方法
Feb 28 Javascript
详解微信小程序调用支付接口支付
Apr 28 Javascript
js 下拉菜单点击旁边收起实现(踩坑记)
Sep 29 Javascript
vue element和nuxt的使用技巧分享
Jan 14 Vue.js
如何在现代JavaScript中编写异步任务
Jan 31 Javascript
微信小程序实现一个简单swiper代码实例
Dec 30 #Javascript
JavaScript switch语句使用方法简介
Dec 30 #Javascript
微信小程序自定义菜单切换栏tabbar组件代码实例
Dec 30 #Javascript
详解Vue的watch中的immediate与watch是什么意思
Dec 30 #Javascript
jQuery模仿ToDoList实现简单的待办事项列表
Dec 30 #jQuery
Vue实现星级评价效果实例详解
Dec 30 #Javascript
vue 中url 链接左边的小图标更改问题
Dec 30 #Javascript
You might like
PHP中遍历stdclass object的实现代码
2011/06/09 PHP
PHP实现合并两个排序链表的方法
2018/01/19 PHP
JQuery优缺点分析说明
2010/06/09 Javascript
基于jquery的bankInput银行卡账号格式化
2012/08/22 Javascript
iframe里的页面禁止右键事件的方法
2014/06/10 Javascript
使用js dom和jquery分别实现简单增删改
2014/09/11 Javascript
js实现向右横向滑出的二级菜单效果
2015/08/27 Javascript
全面解析Bootstrap布局组件应用
2016/02/22 Javascript
js的各种排序算法实现(总结)
2016/07/23 Javascript
Vue.js绑定HTML class数组语法错误的原因分析
2016/10/19 Javascript
原生JS实现不断变化的标签
2017/05/22 Javascript
Angular实现较为复杂的表格过滤,删除功能示例
2017/12/23 Javascript
微信小程序三级联动选择器使用方法
2020/05/19 Javascript
弱类型语言javascript中 a,b 的运算实例小结
2019/08/07 Javascript
js实现自定义右键菜单
2020/05/18 Javascript
Collatz 序列、逗号代码、字符图网格实例
2017/06/22 Python
使用Python处理Excel表格的简单方法
2018/06/07 Python
将Python文件打包成.EXE可执行文件的方法
2019/08/11 Python
MxNet预训练模型到Pytorch模型的转换方式
2020/05/25 Python
用python计算文件的MD5值
2020/12/23 Python
详解css3中 text-fill-color属性
2019/07/08 HTML / CSS
详解px单位html5响应式方案
2018/03/08 HTML / CSS
AmazeUI的下载配置与Helloworld的实现
2020/08/19 HTML / CSS
HTML5 body设置全屏背景图片的示例代码
2020/12/08 HTML / CSS
实习生的自我评价
2014/01/08 职场文书
大二自我鉴定
2014/01/31 职场文书
2014年征兵标语
2014/06/20 职场文书
乡镇爱国卫生月活动总结
2014/06/25 职场文书
2014年企业党建工作总结
2014/12/18 职场文书
2014年图书馆个人工作总结
2014/12/18 职场文书
婚礼答谢词
2015/01/04 职场文书
2015年秋季开学典礼校长致辞
2015/07/16 职场文书
2016全国“质量月”活动标语口号
2015/12/26 职场文书
详解JVM系列之内存模型
2021/06/10 Javascript
国庆节到了,利用JS实现一个生成国庆风头像的小工具 详解实现过程
2021/10/05 Javascript
JAVA API 实用类 String详解
2021/10/05 Java/Android