前言
一句话介绍:它是可以一行命令将代码更新到服务器的脚本
轻量级更新方案最开始源于掘金的文章,后来我们从零实现了一个更新脚本,并且已在是生产环境中进行使用很长时间,算是非常稳定的版本,个人认为轻量化更新方案是非常使用小型开发团队
现在我们切换到了gitlab的CI/CD。所以这种方案已经不再是我们的主流方案,但是一路使用过来,非常稳定的解决了更新问题,还是非常不错的
优点: 快速,稳定,自动备份指定文件夹(灵活性高,但是需要自己实现)
缺点:需要手动回滚(自动回滚需要编码),相对来说没那么规范,没有留下记录,敏感数据存储在电脑中,配置文件可以git
忽略
核心流程
- 确认并打包项目
- 通过
node-ssh
连接线上服务器 - 将打包代码指定名称进行压缩
- 备份之前的代码,删除以前的代码包,并解压压缩包
- 删除本次打包代码,断开ssh链接
如何使用
-
将仓库文件放入项目
-
安装依赖
npm install node-ssh inquirer archiver -D
- 修改
upload.config.js
内容 - 增加脚本命令
"upload": "node build/upload.js"
- 运行命名,验证是否功能正常
核心代码
build/upload.js
const fs = require('fs')
const path = require('path')
const { NodeSSH } = require('node-ssh')
const archiver = require('archiver')
const inquirer = require('inquirer')
const exec = require('child_process').exec
const ssh = new NodeSSH()
const uploadFun = require('../upload.js')
/**
* 获取当前平台
*/
let objName = process.argv[2] // 更新名字
let startTime = null // 程序开始更新的时间
// 获取上传服务器配置
let config = uploadFun(objName)
const verifyList = [
{
type: 'input',
message: '您正在更新到线上环境,请确认接口域名',
name: 'objName',
},
]
inquirer.prompt(verifyList).then(() => {
uploadBuild()
})
function uploadBuild() {
startTime = new Date()
console.log(`${objName}开始更新`)
let buildcmd = exec(config.buildScript, (error, stdout, stderr) => {
if (!error) {
console.log('打包完成', stdout)
app()
} else {
console.error('打包出现错误', stderr)
process.exit(0)
}
})
buildcmd.stdout.on('data', (data) => {
console.log(data.toString())
})
}
/**
* 通过ssh链接服务器
*/
function app() {
ssh
.connect({
host: config.host,
username: config.username,
password: config.password,
})
.then((res) => {
// 上传代码压缩包
uploadData()
})
.catch((err) => {
console.log(err)
})
}
/**
* 上传代码 压缩现有代码
*/
function uploadData() {
// 创建文件输出流
let output = fs.createWriteStream(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`)
// 设置压缩级别
let archive = archiver('zip', {
zlib: {
level: 8,
},
})
// 存档警告
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
console.warn('stat故障和其他非阻塞错误')
} else {
throw err
}
})
// 存档出错
archive.on('error', function(err) {
throw err
})
// 通过管道方法将输出流存档到文件
archive.pipe(output)
archive.directory(`${path.join(__dirname, '../')}${config.buildPath}/${config.buildobj}`, '/')
archive.finalize()
// 文件输出流结束
output.on('close', function() {
console.log(`总共 ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB,完成源代码压缩`)
ssh
.putFile(
`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`,
`${config.uploadDir}/${config.objname}.zip`
)
.then(() => {
console.log('程序zip上传成功,判断线上是否需要备份')
runcmd()
})
.catch((err) => {
console.log(err)
})
})
}
/**
* 执行ssh命令 判断当前是否存在备份
*/
function runcmd() {
ssh
.execCommand('ls', {
cwd: config.uploadDir,
})
.then((res) => {
if (res.stdout) {
let fileList = res.stdout.split('\n')
if (config.objname == config.backObject) {
if (fileList.includes(config.objname)) {
console.log('当前更新为线上正常环境,开始进行备份')
backupData()
} else {
console.log('当前更新为线上正常环境,并且是第一次,将跳过备份')
cmdunzip()
}
} else {
console.log('当前为测试环境,无需备份,直接解压上传压缩包')
cmdunzip()
}
} else if (res.stderr) {
console.log('查询指定目录失败')
} else {
console.log('ssh链接发生了错误')
}
})
}
/**
* 备份项目
*/
function backupData() {
ssh
.execCommand(`mv ${config.objname} backup/${config.objname}_backup${new Date().getTime()}`, {
cwd: config.uploadDir,
})
.then((res) => {
if (res.stderr) {
console.log('备份发生错误', res.stderr)
} else {
console.log('完成备份,解压最新代码')
cmdunzip()
}
})
.catch((err) => {
console.log('备份发生未知链接错误', err)
})
}
/**
* 解压最新代码zip
*/
function cmdunzip() {
// 解压程序
ssh
.execCommand(
`rm -rf ${config.objname} && unzip -o -d ${config.uploadDir}/${config.objname} ${config.objname}.zip && rm -f ${config.objname}.zip`,
{
cwd: config.uploadDir,
}
)
.then(() => {
console.log(`项目包完成解压,${config.objname}项目部署成功了!`)
console.log(`项目更新时长${(new Date().getTime() - startTime.getTime()) / 1000}s`)
return deletelocalFile().then(() => {
console.log('本地缓存zip清除完毕')
})
})
.then(() => {
ssh
.execCommand(`rm -rf ${config.objname}/static/.DS_Store`, {
cwd: config.uploadDir,
})
.then(() => {
console.log('线上项目.DS_Store删除完成')
ssh.dispose()
process.exit(0)
})
.catch((err) => {
console.log(err)
})
})
.catch((err) => {
console.log('解压出现错误', err)
})
}
/**
*删除本地生成的压缩包
*/
function deletelocalFile() {
return new Promise((resolve, reject) => {
fs.unlink(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`, (err) => {
if (err) {
reject(err)
throw err
} else {
resolve()
}
})
})
}
配置文件
upload.config.js
// 打包核心配置文件
let Available = ['dist-a', 'dist-b'] // dist-a 环境a代码包 dist-b 环境b代码包 npm run upload dist-a
/**
* 获取更新配置
* @param {String} objName 当前更新名称
* @returns
*/
module.exports = (objName) => {
if (!Available.includes(objName)) {
console.log('当前项目不存在您输入的更新命令,请检查更新名称')
process.exit(0)
}
return {
host: 'xx.xx.xx.xx', // 服务器地址
username: 'root',
password: 'xxxxxxxxxx',
buildPath: '', // 本地打包项目地址(多层路径用这个)
buildobj: 'dist', // 本地打包文件名称
uploadDir: '/xx/xx/xx', // 服务端项目地址
objname: objName, // 打包项目名称
backObject: 'objName', // 备份的文件夹名称
buildScript: 'npm run build' // 更新命令
}
}
触发命令
最后在package.json增加一行命令,运行前面的脚本文件
"scripts": {
// ......
"upload": "node build/upload.js"
},
依赖版本
因为更新脚本是在项目里面的,所以需要额外安装依赖
推荐版本号
"node-ssh": "^12.0.0",
"inquirer": "^7.3.3",
"archiver": "^3.1.1",
实际使用
npm run upload xxxx // 线上代码文件夹名称
upload 命令后面的字符串就是服务器上的文件夹名称,这里为了防止更新命名敲错了,需要首先在upload.config.js
中进行更新白名单声明,如果配置都正确的情况下,你就可以看到,这就代表成功了~
最后
脚本文件还存在很高的上限,可以优化一下备份部分的备份代码生成规则,再增加一个回滚代码的脚本,就可以实现线上的无感知回滚了
如果使用中遇到了什么问题,请到QQ群 530496237,一起吹吹水