基于node環(huán)境的前端項目遠程部署腳本

前言

有些時候,個人或者公司開發(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)(本人場景用不上):

  1. 部署日志
  2. 防止操作沖突
  3. 回滾支持指定版本
  4. 多環(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),歡迎提建議

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容