使用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的gzip静态压缩方法
Jan 05 Javascript
动态创建样式表在各浏览器中的差异测试代码
Sep 13 Javascript
JS下拉框内容左右移动效果的具体实现
Jul 10 Javascript
javascript中数组array及string的方法总结
Nov 28 Javascript
javascript实现表格排序 编辑 拖拽 缩放
Jan 02 Javascript
js上传图片及预览功能实例分析
Apr 24 Javascript
结合Vue控制字符和字节的显示个数的示例
May 17 Javascript
JS实现图片转换成base64的各种应用场景实例分析
Jun 22 Javascript
java遇到微信小程序 "支付验证签名失败" 问题解决
Dec 22 Javascript
微信小程序使用GoEasy实现websocket实时通讯
May 19 Javascript
vue+iview实现分页及查询功能
Nov 17 Vue.js
vue cli4中mockjs在dev环境和build环境的配置详情
Apr 06 Vue.js
微信小程序实现一个简单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实现下载文件的两种方法
2013/07/05 PHP
php中OR与|| AND与&&的区别总结
2013/10/26 PHP
js下用层来实现select的title提示属性
2010/02/23 Javascript
WEB 浏览器兼容 推荐收藏
2010/05/14 Javascript
如何获取select下拉框的值(option没有及有value属性)
2013/11/08 Javascript
用json方式实现在 js 中建立一个map
2014/05/02 Javascript
小结Node.js中非阻塞IO和事件循环
2014/09/18 Javascript
JS禁用页面上所有控件的实现方法(附demo源码下载)
2015/12/17 Javascript
js获取Get值的方法
2016/09/29 Javascript
使用 jQuery.ajax 上传带文件的表单遇到的问题
2016/10/31 Javascript
JS中判断null的方法分析
2016/11/21 Javascript
Vue组件化通讯的实例代码
2017/06/23 Javascript
详解angular2 控制视图的封装模式
2018/12/27 Javascript
NodeJs生成sitemap站点地图的方法示例
2019/06/11 NodeJs
在Vue项目中使用Typescript的实现
2019/12/19 Javascript
[01:17]辉夜杯战队访谈宣传片—EHOME
2015/12/25 DOTA
[44:30]完美世界DOTA2联赛PWL S2 GXR vs Magma 第一场 11.25
2020/11/26 DOTA
Python中pip安装非PyPI官网第三方库的方法
2015/06/02 Python
利用python编写一个图片主色转换的脚本
2017/12/07 Python
Windows10+anacond+GPU+pytorch安装详细过程
2020/03/24 Python
Django数据库操作之save与update的使用
2020/04/01 Python
Pytorch 解决自定义子Module .cuda() tensor失败的问题
2020/06/23 Python
解决redis与Python交互取出来的是bytes类型的问题
2020/07/16 Python
Numpy(Pandas)删除全为零的列的方法
2020/09/11 Python
python 写一个性能测试工具(一)
2020/10/24 Python
英国最大的奢侈品零售网络商城:Flannels
2016/09/16 全球购物
亚洲最大的运动鞋寄售店:KicksCrew
2020/11/26 全球购物
华为python面试题
2016/05/03 面试题
医院护理人员的自我评价分享
2013/10/04 职场文书
通用求职信范文模板分享
2013/12/27 职场文书
英语教学随笔感言
2014/02/20 职场文书
招商银行收入证明
2015/06/17 职场文书
假如给我三天光明读书笔记
2015/06/26 职场文书
同步小康驻村工作简报
2015/07/20 职场文书
超外差式晶体管收音机的组装与统调
2021/04/22 无线电
python中filter,map,reduce的作用
2022/06/10 Python