使用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 1.3 和 Validation 验证插件1.5.1
Jul 09 Javascript
学习ExtJS 访问容器对象
Oct 07 Javascript
让FireFox支持innerText的实现代码
Dec 01 Javascript
JS+CSS实现精美的二级导航效果代码
Sep 17 Javascript
浅谈javascript的Touch事件
Sep 27 Javascript
jQuery实现页面评论栏中访客信息自动填写功能的方法
May 23 Javascript
js基于cookie方式记住返回页面用法示例
May 27 Javascript
bootstrap弹出层的多种触发方式
May 10 Javascript
一篇文章让你彻底弄懂JS的事件冒泡和事件捕获
Aug 14 Javascript
Vue表单类的父子组件数据传递示例
May 03 Javascript
layer弹窗在键盘按回车将反复刷新的实现方法
Sep 25 Javascript
vue3自定义dialog、modal组件的方法
Jan 04 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制作静态网站的模板框架(四)
2006/10/09 PHP
php查询ip所在地的方法
2014/12/05 PHP
php获取系统变量方法小结
2015/05/29 PHP
PHP的Yii框架入门使用教程
2016/02/15 PHP
详解PHP的Yii框架的运行机制及其路由功能
2016/03/17 PHP
yii2 modal弹窗之ActiveForm ajax表单异步验证
2016/06/13 PHP
php 使用fopen函数创建、打开文件详解及实例代码
2016/09/24 PHP
iis6+javascript Add an Extension File
2007/06/13 Javascript
JavaScript 学习技巧
2010/02/17 Javascript
js鼠标滑轮滚动事件绑定的简单实例(兼容主流浏览器)
2014/01/14 Javascript
node+express+jade制作简单网站指南
2014/11/26 Javascript
JS实现网页表格自动变大缩小的方法
2015/03/09 Javascript
jQuery定义插件的方法
2015/12/18 Javascript
jQueryUI DatePicker 添加时分秒
2016/06/04 Javascript
关于javascript中限定时间内防止按钮重复点击的思路详解
2016/08/16 Javascript
实例解析jQuery工具函数
2016/12/01 Javascript
Bootstrap源码学习笔记之bootstrap进度条
2016/12/24 Javascript
浅谈JS中的常用选择器及属性、方法的调用
2017/07/28 Javascript
JS中使用media实现响应式布局
2017/08/04 Javascript
vue-simple-uploader上传成功之后的response获取代码
2020/09/07 Javascript
[10:53]2018DOTA2国际邀请赛寻真——EG
2018/08/11 DOTA
python实现的防DDoS脚本
2011/02/08 Python
python服务器端收发请求的实现代码
2014/09/29 Python
Python编写屏幕截图程序方法
2015/02/18 Python
详解Python3中字符串中的数字提取方法
2017/01/14 Python
python调用系统ffmpeg实现视频截图、http发送
2018/03/06 Python
python引用(import)某个模块提示没找到对应模块的解决方法
2019/01/19 Python
Django如何实现网站注册用户邮箱验证功能
2019/08/14 Python
Python大数据之使用lxml库解析html网页文件示例
2019/11/16 Python
python3检查字典传入函数键是否齐全的实例
2020/06/05 Python
python 解决mysql where in 对列表(list,,array)问题
2020/06/06 Python
美国新兴城市生活方式零售商:VILLA
2017/12/06 全球购物
买卖协议书范本
2014/04/21 职场文书
物理分数没达标检讨书
2014/09/13 职场文书
SQL Server代理:理解SQL代理错误日志处理方法
2021/06/30 SQL Server
Python学习之迭代器详解
2022/04/01 Python