基于node环境的前端项目远程部署脚本

前言

有些时候,个人或者公司开发服务器并没有jekins持续集成,是手动ssh连接服务器上传项目包,压缩解压备份一套流程下来实在是低效,而且虽然是开发服务器但是有时候还是需要备份包才安心,对旧包备份就更加繁琐了,还容易出错。
如果是小公司没有开发服务器,还在用生产服务器装git更新代码或者手动拷贝的方式,就更需要这种脚本来提高效率啦。
虽然网上有很多类似的文章,但是我相信我的版本是轻量脚本中最完善滴!
话不多说,正文开始

首先写一下我们要实现哪些功能点

1.支持自动打包项目并压缩

2.支持参数可配置方便迁移多项目使用

3.支持密码用户密码+ssh私钥免密码登录服务器

4.支持压缩产物部署并备份旧项目

5.支持项目回滚

6.本地产物部署后自动清理,远程备份文件可按配置自动清理(有效期)

如果恰好你时间很多,可以考虑实现(本人场景用不上):

  1. 部署日志
  2. 防止操作冲突
  3. 回滚支持指定版本
  4. 多环境配置

使用形态

如图所示,点击命令一键部署,
点击一个按钮或者输入一条命令,实现一键式自动打包-上传-部署/回滚-备份流程


其实本质就是ssh链接服务器,sftp传输文件,相当简单,只不过实际开发完善了许多细节而已。

涉及的库

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') // 控制台打印带颜色输出,也可以不引入

核心代码实现

先定义一个配置文件config.js,存放服务器ip、项目路径等信息

module.exports = ({
host: 'xxx.xxx.xxx.xx',
username: 'yourName',
password: 'password',
privateKey:'C:\\Users\\kob\\.ssh\\id_rsa',// 本地ssh私钥文件路径,优先级大于密码
port: 22,
backupExpires:3,// 备份时效天为单位,不填或者0默认永不失效,即备份永远不会清理
pathUrl:'/home/bok',// 服务器项目部署路径(一般为dist目录父级)
backupKeyword:'backup',// 备份文件命名关键字(任意字符皆可,默认backup即可)
localPkgPath:'src/dist',
localPkgName:'dist' // 本地打包产物文件夹名(一般为dist,可自定义,不可包含上面backupKeyword字段)
})

核心代码

新建脚本文件

在配置文件同级别新建一个index.js(名字路径随意皆可,引入配置文件注意下就行)

init方法初始化,先对配置文件做一下校验再创建ssh链接(因为很可能配置文件跟脚本是不同的人管理的,校验下比较好,万一有不靠谱的同事把部署路径写错了就完犊子了)

function init() {
checkLocalConfig()
exists(`${config.localPkgName}`, function (exists) {
    if (!exists && !isRollback) {
        log_break(`config.localPkgName对应产物路径无效`)
    }
})
log.gre(`开始执行${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方法发起链接,注意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`)
}
}

// 根据配置,对旧的备份文件做清理

const backupClear = async () => {
const configExprires = Math.ceil(config.backupExpires)
const backupArr = await getAllBackup()
// 过滤出备份文件
const arr = backupArr.filter(e => {
    return e.includes(config.backupKeyword)
})
// 根据配置计算失效时间,这里以天为粒度
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(`根据配置,以下${configExprires}天前备份已失效被清除:${backupStr}`)
        }
    } catch (err) {
        log_break(err)
    }
} else {
    log.gre('暂无过期备份需清除')
}
}

回滚是不会走到这里的,这一步属于部署逻辑,这块就是文件包压缩、上传、备份、解压

打包产物压缩

其实也可以递归上传文件夹,可少引入一个压缩库,不过我这里就偷个懒了

const compressLocalFile = () => {
    log.gre(`本地打包产物压缩开始\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(`压缩结束,包大小:${(archive.pointer() / 1024 / 1024).toFixed(2)}mb\n`)
        uploadZip(outputPath)
    })
    output.on('end', () => {
        log.gre(`数据处理完毕\n`)
    })
    archive.pipe(output)
    archive.directory(distPath, `/dist`)
    archive.finalize()
}

压缩包上传完成后,咱们就开始更新部署路径里的文件夹(其实本质就是替换文件而已,so easy~)

  • 这里如果是回滚,就会先获取最新的备份文件,见getLastBackup()方法,然后删除原项目并将最新备份文件命名为正确项目名

  • 如果是正常部署,先对原有项目按规则命名备份,然后解压上传的包,再删除服务器压缩包,最好用

     const remoteFileUpdate = async () => {
     let cmd
     log.gre('执行远程更新命令\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)
     }
    
     }
    

功能实现,欢迎提建议

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容