問題:
公司有個工具型項目使用node.js 開發,需要部署到客戶的服務器中,遇到的問題:
1、客戶的服務器沒有外網。環境配置,依賴安裝等都比較麻煩,只能手工上傳,最好能一個文件直接搞定;
2、直接包源碼部署到客戶的機器中,存在源碼泄露的風險。
方案:
使用pkg npm包可以很好的解決我們以上的問題。
先參考前人的經驗 : Egg.js線上部署——利用pkg打包Egg.js工程
pkg原理:
pkg打包工具主要會按平台(支持window、mac、linux)分別打包。
pkg中會包含node的可執行文件,還會包含你要打包進去的代碼。代碼通過一個虛擬的文件系統把所有的代碼和資源文件都掛載到 /snapshot/${被打包項目的文件夾名} 下面(pkg hack了 fs 的很多方法,攔截文件操作,如果發現讀的文件路徑是在掛載目錄下就特殊處理,返回打包進去的文件信息,如果不在掛載目錄下,則按node默認邏輯進行)
pkg 會根據被打包項目的package.json 中的licence聲明判斷是否要對源碼進行編譯,npm上安裝的依賴包基本都是開源的,會以源碼的形式打包;而用戶的源碼如果沒有聲明開源的協議,則會把js文件編譯為v8字節碼進行保存(在項目中也就沒法通過fs讀取到源碼,會給出拋錯,即使是破解安裝包也只能拿到v8字節碼)
實現:
1、安裝(也可以考慮只安裝到項目內,通過npm script 調用打包)
npm install pkg -g
2、在egg配置文件中把涉及到寫文件的路徑都移到包外(pkg的虛擬文件系統只是用來應對讀的行為,所有寫相關都得移出包外)
// 通過process.cwd()獲取當前執行文件執行的路徑 config.rundir = process.cwd() + '/run';// 配置執行時臨時文件的路徑 config.logger = { dir: path.join(process.cwd(), 'logs', logDir),//配置普通日志文件地址 }; config.customLogger = { scheduleLogger: { file: path.join(process.cwd(), 'logs', logDir, 'egg-schedule.log'),//配置定時任務日志的地址 }, }; config.static = { // 必須把public移出項目,否則在pkg的包中egg的static中間件會有對public操作(確保文件夾),會有拋錯 prefix: '/', dir: process.cwd() + '/public',//配置靜態文件的地址 };
3、修改package.json文件
{ ... "bin": "pkg-entry.js", // 執行包的入口文件,可執行包啟動的時候默認會調用該文件 "pkg": {// 以下主要是聲明那些文件需要被打包(pkg會解析require中的靜態路徑,但在egg.js中很多文件都是通過框架引用的,無法依賴解析) "scripts": [ // 這里是聲明需要打包的js文件,這里的聲明的js文件都會被編譯為v8字節碼(建議主動聲明,不要依賴pkg自動引入) "./app/**/*.js", "./config/**/*.js", "./normalJs/**/*.js", // 不只是egg的文件,只要要用到就要聲明打包 "./app.js", "./agent.js" ], "assets": [ // 這里是聲明需要打包的靜態文件(即使有js文件也不進行編譯)。 "./lib/**/*",// lib中是我打算開放的一些egg組件,所有不需要編譯 "./app/public/**/*",// 如果要把前端靜態文件打包進來,就直接聲明(但是在egg中static中間件會有拋錯,需要hack egg或者 hack pkg) "./node_modules/**/*"// npm安裝的所有依賴包全部打包進來,不要依賴自動引入,很容易導致部分文件沒打包,出現各種意料外的錯誤 ] }, }
4、增加入口文件pkg-entry.js(名字保持和package.json中一致)
const fs = require('fs'); // 如果是egg的ts項目,由於egg-script會給ts項目通過-r引入sourcemap的注入文件,但是pkg的spawn不支持,所以把項目標識為飛ts // 如果不是ts項目忽略一下兩行 const pkgInfo = require('./package'); pkgInfo.egg.typescript = false; // 防止egg-script識別為 typescript 自動添加soucemap支持(--require 在pkg的spawn中不支持) //我自己的工具包的配置文件是直接打包到安裝包里面的,這樣就不方便修改配置了。於是把提供給運維配置的配置都適用dotenv來配置,以下引入dotenv的預執行腳本 //也可以考慮把配置文件放到包外,不過因為包內執行包外js,會增加被攻擊的風險 require('dotenv/config'); // 由於egg-script是默認以當前執行proccess.cwd() 路徑為默認項目的,打包后需要每次輸入 /snapshot/${項目文件夾名} 作為指定目錄 // 所以,以下為修改參數,自動嵌入“/snapshot/${項目文件夾名}” const baseDir = '/snapshot/' + fs.readdirSync('/snapshot')[0]; console.log('baseDir:', baseDir); // 當 start 的時候,自動嵌入bashDir為 /snapshot/${項目文件夾名} const startIndex = process.argv.indexOf('start'); if (startIndex > -1) { process.argv = [].concat( process.argv.slice(0, startIndex + 1), baseDir, process.argv.slice(startIndex + 1), ); } // 然后直接調起egg-scripts執行 require('./node_modules/egg-scripts/bin/egg-scripts.js');
5、 執行打包
## --targets 用於制定平台和node版本,不指定時默認為3個平台以package.json中的node版本配置為准 ## --out-path 指定執行包輸出文件夾,默認為當前文件夾 ## --debug 用於調試,可了解哪些文件被打包 pkg . --targets node8-linux-x64 --out-path /usr/dist --debug
6、運行
> chmod a+x ./appName #給可執行包增加執行權限 > ./appName start # 啟動項目,除項目路徑其它參數都和egg-scripts一致 可以用--title指定egg服務名 >./appName stop # 關閉項目(會關閉當前服務器的所有egg服務,如果有多個,最好用--title來指定要關閉的項目)
ps:
如果需要動態配置或者包中使用了c++編譯的node依賴包,可參考該文章 Egg.js線上部署——利用pkg打包Egg.js工程 。
可能遇到問題:
第一次打包的時候,會遇到下包很慢很可能超時的問題。如下:
到https://github.com/zeit/pkg-fetch/releases下載對應的包,然后
~/.pkh-cache/2.5/目錄下,改名為fetched-v8.11.3-macos-x64(參考運行時下的包名字改)即可。參考https://github.com/zeit/pkg/issues/419