使用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 相关文章推荐
jQuery DOM操作小结与实例
Jan 07 Javascript
Javascript异步编程的4种方法让你写出更出色的程序
Jan 17 Javascript
js操作table示例(个人心得)
Nov 29 Javascript
js的2种继承方式详解
Mar 04 Javascript
详解JavaScript中循环控制语句的用法
Jun 03 Javascript
纯HTML5制作围住神经猫游戏-附源码下载
Aug 23 Javascript
浅谈JS使用[ ]来访问对象属性
Sep 21 Javascript
JavaScript实现倒计时跳转页面功能【实用】
Dec 13 Javascript
vue中的模态对话框组件实现过程
May 01 Javascript
微信小程序—setTimeOut定时器的问题及解决
Jul 26 Javascript
JS常用正则表达式超全集(密码强度校验,金额校验,IE版本,IPv4,IPv6校验)
Feb 03 Javascript
js实现飞机大战小游戏
Aug 26 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
一个图形显示IP的PHP程序代码
2007/10/19 PHP
Eclipse中php插件安装及Xdebug配置的使用详解
2013/04/25 PHP
Add Formatted Text to a Word Document
2007/06/15 Javascript
Draggable Elements 元素拖拽功能实现代码
2011/03/30 Javascript
5秒后跳转效果(setInterval/SetTimeOut)
2013/05/03 Javascript
div当滚动到页面顶部的时候固定在顶部实例代码
2013/05/27 Javascript
js函数返回多个返回值的示例代码
2013/11/05 Javascript
JavaScript生成福利彩票双色球号码
2015/05/15 Javascript
Javascript基础之数组的使用
2016/05/13 Javascript
jQuery事件委托之Safari
2016/07/05 Javascript
轻松掌握JavaScript中介者模式
2016/08/26 Javascript
Javascript实现base64的加密解密方法示例
2017/06/27 Javascript
Javascript中toFixed计算错误(依赖银行家舍入法的缺陷)解决方法
2017/08/22 Javascript
JavaScript数组push方法使用注意事项
2017/10/30 Javascript
JavaScript时间日期操作实例小结【5个示例】
2018/12/22 Javascript
swiper Scrollbar滚动条组件详解
2019/09/08 Javascript
ES6中new Function()语法及应用实例分析
2020/02/19 Javascript
react组件基本用法示例小结
2020/04/27 Javascript
javascript读取本地文件和目录方法详解
2020/08/06 Javascript
IDEA配置jQuery, $符号不再显示黄色波浪线的问题
2020/10/09 jQuery
编写Python的web框架中的Model的教程
2015/04/29 Python
利用Python进行异常值分析实例代码
2017/12/07 Python
python 计算数组中每个数字出现多少次--“Bucket”桶的思想
2017/12/19 Python
基于Python pip用国内镜像下载的方法
2018/06/12 Python
使用django-guardian实现django-admin的行级权限控制的方法
2018/10/30 Python
python抓取需要扫微信登陆页面
2019/04/29 Python
Python基于jieba, wordcloud库生成中文词云
2020/05/13 Python
keras多显卡训练方式
2020/06/10 Python
python 对象真假值的实例(哪些视为False)
2020/12/11 Python
详解HTML5中rel属性的prefetch预加载功能使用
2016/05/06 HTML / CSS
2013年办公室秘书的个人自我鉴定
2013/10/24 职场文书
《富饶的西沙群岛》教学反思
2014/04/09 职场文书
2015年“七七卢沟桥事变”纪念活动总结
2015/03/24 职场文书
孙振耀退休感言
2015/08/01 职场文书
Python内置包对JSON文件数据进行编码和解码
2022/04/12 Python
关于对TypeScript泛型参数的默认值理解
2022/07/15 Javascript