前言
有些時候,個人或者公司開發(fā)服務器并沒有jekins持續(xù)集成,是手動ssh連接服務器上傳項目包,壓縮解壓備份一套流程下來實在是低效,而且雖然是開發(fā)服務器但是有時候還是需要備份包才安心,對舊包備份就更加繁瑣了,還容易出錯。
如果是小公司沒有開發(fā)服務器,還在用生產(chǎn)服務器裝git更新代碼或者手動拷貝的方式,就更需要這種腳本來提高效率啦。
雖然網(wǎng)上有很多類似的文章,但是我相信我的版本是輕量腳本中最完善滴!
話不多說,正文開始
首先寫一下我們要實現(xiàn)哪些功能點
1.支持自動打包項目并壓縮
2.支持參數(shù)可配置方便遷移多項目使用
3.支持密碼用戶密碼+ssh私鑰免密碼登錄服務器
4.支持壓縮產(chǎn)物部署并備份舊項目
5.支持項目回滾
6.本地產(chǎn)物部署后自動清理,遠程備份文件可按配置自動清理(有效期)
如果恰好你時間很多,可以考慮實現(xiàn)(本人場景用不上):
- 部署日志
- 防止操作沖突
- 回滾支持指定版本
- 多環(huán)境配置
使用形態(tài)
如圖所示,點擊命令一鍵部署,
點擊一個按鈕或者輸入一條命令,實現(xiàn)一鍵式自動打包-上傳-部署/回滾-備份流程
其實本質(zhì)就是ssh鏈接服務器,sftp傳輸文件,相當簡單,只不過實際開發(fā)完善了許多細節(jié)而已。
涉及的庫
const fs = require('fs')
const {exists} = require('fs')
const path = require('path')
const archiver = require('archiver') // 壓縮插件,其實可以用遞歸來拷貝文件夾就不用引入了,偷個懶
const { NodeSSH } = require('node-ssh') // 核心庫,ssh連接的,就這個是必須的
const sd = require('silly-datetime') // 時間處理的庫,其實可以自己處理格式,也是偷懶
const chalk = require('chalk') // 控制臺打印帶顏色輸出,也可以不引入
核心代碼實現(xiàn)
先定義一個配置文件config.js,存放服務器ip、項目路徑等信息
module.exports = ({
host: 'xxx.xxx.xxx.xx',
username: 'yourName',
password: 'password',
privateKey:'C:\\Users\\kob\\.ssh\\id_rsa',// 本地ssh私鑰文件路徑,優(yōu)先級大于密碼
port: 22,
backupExpires:3,// 備份時效天為單位,不填或者0默認永不失效,即備份永遠不會清理
pathUrl:'/home/bok',// 服務器項目部署路徑(一般為dist目錄父級)
backupKeyword:'backup',// 備份文件命名關鍵字(任意字符皆可,默認backup即可)
localPkgPath:'src/dist',
localPkgName:'dist' // 本地打包產(chǎn)物文件夾名(一般為dist,可自定義,不可包含上面backupKeyword字段)
})
核心代碼
新建腳本文件
在配置文件同級別新建一個index.js(名字路徑隨意皆可,引入配置文件注意下就行)
init方法初始化,先對配置文件做一下校驗再創(chuàng)建ssh鏈接(因為很可能配置文件跟腳本是不同的人管理的,校驗下比較好,萬一有不靠譜的同事把部署路徑寫錯了就完犢子了)
function init() {
checkLocalConfig()
exists(`${config.localPkgName}`, function (exists) {
if (!exists && !isRollback) {
log_break(`config.localPkgName對應產(chǎn)物路徑無效`)
}
})
log.gre(`開始執(zhí)行${isRollback ? '回滾' : '部署'}操作,時間戳:${curTime}\n`)
serverConnect()
}
function checkLocalConfig() {
try {
config = require('./deploy.config_local')
for (let key in config) {
if (!config[key] && key !== 'privateKey') {
log_break(`配置文件不完整,請補充config.${key}\n`)
}
}
log.gre('配置文件校驗通過~')
} catch (err) {
log_break(`配置文件缺失或路徑有誤,請在項目根目錄配置deploy.config.js文件\n`)
}
}
校驗過后,在init方法中的serverConnect方法發(fā)起鏈接,注意checkServerPakg(),別忘記校驗下服務器是否存在目標部署路徑
async function serverConnect() {
try {
await ssh.connect({
host: config.host,
username: config.username,
port: config.port,
...config.privateKey ? {privateKey: fs.readFileSync(path.join(config.privateKey), 'utf8')} : {password: config.password}
})
log.gre('ssh連接成功\n')
await checkServerPakg()
// 判斷是否要對舊的備份文件做清理做清理
if (config.backupExpires && typeof config.backupExpires === "number") await backupClear()
if (isRollback) {
await remoteFileUpdate()
} else {
compressLocalFile()
}
} catch (err) {
log_break(`SSH連接失敗:${err}\n`)
}
}
// 根據(jù)配置,對舊的備份文件做清理
const backupClear = async () => {
const configExprires = Math.ceil(config.backupExpires)
const backupArr = await getAllBackup()
// 過濾出備份文件
const arr = backupArr.filter(e => {
return e.includes(config.backupKeyword)
})
// 根據(jù)配置計算失效時間,這里以天為粒度
const expriesTime = Number(getTime(-configExprires))
// 找出過期的備份文件名以空格分隔存為字符串
let backupStr = ''
for (const v of arr) {
// 倒序取出當前備份年月日,配置路徑變化會導致位置變化不可以
let tempTime = Number(v.slice(-14, -6))
if (tempTime < expriesTime) backupStr += ' ' + v
}
if (backupStr) {
try {
const res = await ssh.execCommand(`rm -f -r ${backupStr}`, {
cwd: `${config.pathUrl}`,
})
if (res.stderr) {
log.red(`${res.stderr}\n`)
} else {
log.yel(`根據(jù)配置,以下${configExprires}天前備份已失效被清除:${backupStr}`)
}
} catch (err) {
log_break(err)
}
} else {
log.gre('暫無過期備份需清除')
}
}
回滾是不會走到這里的,這一步屬于部署邏輯,這塊就是文件包壓縮、上傳、備份、解壓
打包產(chǎn)物壓縮
其實也可以遞歸上傳文件夾,可少引入一個壓縮庫,不過我這里就偷個懶了
const compressLocalFile = () => {
log.gre(`本地打包產(chǎn)物壓縮開始\n`)
// 設置本地dist文件絕對路徑
const distPath = path.resolve(__dirname, `${config.localPkgName}`)
const outputPath = `${__dirname}/${config.localPkgName}${curTime}.zip`
const output = fs.createWriteStream(outputPath)
const archive = archiver('zip', {
zlib: {level: 9},
}).on('error', (err) => {
throw err
})
output.on('close', (err) => {
if (err) log_break(`文件壓縮出錯:${JSON.stringify(err)}\n`)
log.gre(`壓縮結(jié)束,包大小:${(archive.pointer() / 1024 / 1024).toFixed(2)}mb\n`)
uploadZip(outputPath)
})
output.on('end', () => {
log.gre(`數(shù)據(jù)處理完畢\n`)
})
archive.pipe(output)
archive.directory(distPath, `/dist`)
archive.finalize()
}
壓縮包上傳完成后,咱們就開始更新部署路徑里的文件夾(其實本質(zhì)就是替換文件而已,so easy~)
這里如果是回滾,就會先獲取最新的備份文件,見getLastBackup()方法,然后刪除原項目并將最新備份文件命名為正確項目名
-
如果是正常部署,先對原有項目按規(guī)則命名備份,然后解壓上傳的包,再刪除服務器壓縮包,最好用
const remoteFileUpdate = async () => { let cmd log.gre('執(zhí)行遠程更新命令\n') if (isRollback) { const lastBackup = await getLastBackup() cmd = `rm -r ${config.localPkgName} && mv ${lastBackup} ${config.localPkgName}` log.gre(`回滾目標版本:${lastBackup}\n`) } else { cmd = `mv ${config.localPkgName} ${config.localPkgName}.${config.backupKeyword}${curTime} && unzip ${config.localPkgName}${curTime}.zip && rm -r ${config.localPkgName}${curTime}.zip` } try { const res = await ssh.execCommand(cmd, {cwd: config.pathUrl}) log.gre(`上傳信息輸出:${res.stdout}\n`) if (!res.stderr) { log.gre(`項目已${isRollback ? '回滾' : '部署'}成功!\n`) // 刪除本地壓縮包 if (!isRollback) fs.unlinkSync(`${__dirname}\\${config.localPkgName}${curTime}.zip`) log_break() } else { log_break(`遠程更新命令出錯:${JSON.stringify(res)}\n`) } } catch (err) { log_break(err) } }
功能實現(xiàn),歡迎提建議