作为一个前端开发,本不需要自己部署项目,奈何天不遂人愿,活最终还是落到了自己头上,刚开始只是部署测试环境,只有一台服务器,手动部署以下也就算了,后面线上环境部署4台服务器,人当时就麻了
对于喜爱摸鱼的我来说,严重耽误我摸鱼的时间,于是乎就在想能否写一个node脚本执行命令自动部署呢,想了一下,还是决定动手试试
最开始是想通过执行shell命令打包项目,然后通过node进行io操作打包,再通过ssh2上传到服务器进行部署
后面一想,都用到了shell,何不干脆直接通过shell命令打包,压缩呢,于是乎,引入了
child_process,使用同步执行命令,手动进度,哈哈哈哈
const { spawnSync } = require('child_process');
const itemPath = "/Volumes/xxxx/" //项目路径
const filePath = "/Users/admin/Desktop/" //项目打包后的路径
function exec(item, cmd) {
const child = spawnSync(cmd, { shell: true, stdio: ['pipe', 'inherit', 'inherit'] });
if (child.status != 0) {
console.log(`[${getNowTime(0)}] ${item}失败`);
}
}
// 打包项目
function build() {
const cmdList = {
'打包中(0%)': 'cd ' + itemPath + ' && ' + runCmd + ' && touch -m -a ' + itemPath,
'打包中(10%)': 'cd ' + filePath + ' && rm -rf build build.zip dist dist.zip && mkdir -p build/.nuxt build/static build/node_modules dist',
'打包中(20%)': '(cd ' + itemPath + '.nuxt/ && tar cf - .) | (cd ' + filePath + 'build/.nuxt && tar xpf -)',
'打包中(30%)': '(cd ' + itemPath + 'static/ && tar cf - .) | (cd ' + filePath + 'build/static && tar xpf -)',
'打包中(40%)': '(cd ' + itemPath + 'node_modules/ && tar cf - .) | (cd ' + filePath + 'build/node_modules && tar xpf -)',
'打包中(50%)': 'cp -rf ' + itemPath + 'package.json ' + filePath + 'build',
'打包中(60%)': 'cp -rf ' + itemPath + 'nuxt.config.js ' + filePath + 'build',
'打包中(70%)': 'cp -rf ' + itemPath + 'config.js ' + filePath + 'build',
'打包中(80%)': 'cp -rf ' + itemPath + 'package-lock.json ' + filePath + 'build',
'打包中(90%)': 'cp -rf ' + itemPath + 'ecosystem.config.js ' + filePath + 'build',
'打包中(100%)': '(cd ' + itemPath + '.nuxt/dist/ && tar cf - .) | (cd ' + filePath + 'dist && tar xpf -)', //xpvf
}
try {
Object.keys(cmdList).forEach(function (item, index) {
exec(item, cmdList[item])
})
} catch (error) {
console.log(error);
}
}
(之所以复制node_modules是因为服务器安装会导致出问题,公司大佬也没解决,先凑合用一下)第一步搞定,项目打包并复制到新的文件夹
第二步压缩文件夹
const fs = require('fs')
const archiver = require('archiver')
function compressFile(targetDir, outputDir, fileName) {
return new Promise((resolve, reject) => {
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); // 如果文件夹不存在,则创建
let output = fs.createWriteStream(`${outputDir}/${fileName}.zip`) // 创建⽂件写⼊流
const archive = archiver('zip', { zlib: { level: 9 } }) // 设置压缩等级
output.on('close', () => {
resolve(' 压缩完成')
}).on('error', (err) => {
reject(console.error(`[${getNowTime(0)}] ${fileName} 压缩失败`, err))
})
archive.pipe(output)
archive.directory(targetDir, fileName) // 存储⽬标⽂件
archive.finalize() // 完成归档
})
}
async function building() {
// 打包
build()
// 压缩
try {
await compressFile(`${filePath}build`, filePath, 'build')
await compressFile(`${filePath}dist`, filePath, 'dist')
} catch (error) {
console.log(error);
}
}
第三步上传到服务器,解压后执行
const Client = require("ssh2").Client;
const uploadBuildFilePath = filePath + 'build.zip' // 本地包地址 需加上文件名和后缀
const uploadDistFilePath = filePath + 'dist.zip' // 本地包地址 需加上文件名和后缀
const serverBuildPath = '/home/xxx/build.zip' //服务器上传地址 需加上文件名和后缀
const serverDistPath = '/home/xxx/dist.zip' //服务器静态资源上传地址 需加上文件名和后缀
const projectBuildPath = '/usr/local/xxx/' //服务器项目运行路径
const projectDistPath = '/xxx/'//服务器静态资源路径
let runCmd = 'npm run build' // 打包命令
// 服务器列表
let serverList = []
// 正式服务器
const proServerList = [
{
name:'static',
cmd: 'su',
shell: 'unzip -o -q ' + serverDistPath + ' -d ' + projectDistPath,
pm2: '',
serverName: 'xxx 静态',
host: '127.0.0.1',
port: '22',
username: 'xxx',
password: 'aaaa',
rootpass: 'ddddd',
serverPath: serverDistPath,
filePath: uploadDistFilePath
},
{
name:'xxxaaa',
cmd: 'su',
shell: 'unzip -o -q ' + serverBuildPath + ' -d ' + projectBuildPath + ' && cd ' + projectBuildPath + 'build && pm2 delete all && npm run pm_4',
pm2: 'npm run pm_4',
serverName: 'xxx 4节点',
host: '127.0.0.1',
port: '22',
username: 'xxxx',
password: 'xxx',
rootpass: 'xxxx',
serverPath: serverBuildPath,
filePath: uploadBuildFilePath
}
// ....
]
// 测试服务器
const devServerList = [
{
cmd: 'su',
shell: 'unzip -o -q /root/build.zip -d /xxx/ && cd /xxx/build && pm2 delete all && npm run pm2-test',
pm2: 'npm run pm2-test',
serverName: 'xxx 测试',
host: '127.0.0.1',
port: '22',
username: 'root',
password: 'admin',
rootpass: 'admin',
serverPath: '/aaa/build.zip',
filePath: uploadBuildFilePath
},
]
function Connect(server, then) {
var conn = new Client();
conn.on("ready", function () {
then(conn);
}).on('error', function (err) {
// console.log("connect error!");
}).on('end', function () {
// console.log("connect end!");
}).on('close', function (had_error) {
// console.log("connect close");
}).connect(server);
}
function UploadFile(server, info, cmd, then) {
let unzip = false
let x = 0
let y = 0
let end = false
progressBarC.run(0)
Connect(server, function (conn) {
conn.sftp(function (err, sftp) {
if (err) {
then(err);
} else {
progressBarC.run(5)
// console.log(`[${getNowTime(0)}] 服务器连接成功`);
// console.log(`[${getNowTime(0)}] 开始上传压缩包`);
sftp.fastPut(info.filePath, info.serverPath, function (err, result) {
if (err) console.log(err);
// console.log(`[${getNowTime(0)}] 压缩包上传成功`);
// console.log(`[${getNowTime(0)}] 开始部署`);
conn.exec(cmd, { pty: true }, function (err, stream) {
if (err) {
then(err, 0);
} else {// end of if
stream.on('data', async function (data, extended) {
if (data.toString().indexOf('Password:') >= 0) {
// console.log(`[${getNowTime(0)}] 切换root`);
stream.write(info.rootpass + '\n');
}
//... 自己的逻辑
}).on('close', async (code, signal) => {
// console.log(`[${getNowTime(0)}] 部署完成`);
then(0, 1)
conn.end();
});
}
});
});
}
});
});
}
function uploadFile(index) {
if (index < serverList.length) {
let item = serverList[index]
let server = {
host: item.host, // 服务器 IP
port: item.port,
username: item.username,
password: item.password
}
UploadFile(server, item, item.cmd, (err, res) => {
if (res > 0) {
index++
uploadFile(index)
} else {
console.log(err);
}
});
}
}
因为要切换root所以,要输入root密码,然后自己根据输出信息判断是否切换成功,再去执行解压操作,然后通过pm2管理进程即可
最后执行这个脚本
async function deploy() {
// npm run deploy 全部部署 自动打包
// 全部部署 或 部分部署
if (process.env.ENV_DEPLOY == 'all') {
serverList = proServerList
} else {
let server = proServerList.find(item => {
return item.name == process.env.ENV_DEPLOY
})
serverList = server
}
// 测试环境部署
if (process.env.NODE_ENV == 'dev') {
serverList = devServerList
runCmd = 'npm run build-test'
}
// 是否需要打包,npm run deploy-build
if(process.env.ENV_BUILD == 'true'){
//打包压缩
await building()
}
// 部署到服务器
if (process.env.ENV_DEPLOY != 'no') {
// 服务器部署
uploadFile(0)
}
}
然后配置一下package.json
"deploy": "cross-env NODE_ENV=production ENV_DEPLOY=all ENV_BUILD=true ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-build": "cross-env NODE_ENV=production ENV_DEPLOY=no ENV_BUILD=true ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-static": "cross-env NODE_ENV=production ENV_DEPLOY=static ENV_BUILD=false ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-107": "cross-env NODE_ENV=production ENV_DEPLOY=107 ENV_BUILD=false ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-181": "cross-env NODE_ENV=production ENV_DEPLOY=181 ENV_BUILD=false ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-150": "cross-env NODE_ENV=production ENV_DEPLOY=150 ENV_BUILD=false ENV_BASE_URL=server.xxx.com node deploy.js",
"deploy-test": "cross-env NODE_ENV=dev ENV_DEPLOY=all ENV_BUILD=true ENV_BASE_URL=192.168.3.39:19083 node deploy.js",
为了方便观察进度,使用@jyeontu/progress-bar 进行设置,最后附上完整代码
const fs = require('fs')
const path = require("path")
const archiver = require('archiver')
const { spawnSync } = require('child_process');
const Client = require("ssh2").Client;
const progressBar = require('@jyeontu/progress-bar');
const itemPath = "/Volumes/aaa/bbb/ccc/" //项目路径
const filePath = "/Users/admin/Desktop/" //项目打包后的路径
const uploadBuildFilePath = filePath + 'build.zip' // 本地包地址 需加上文件名和后缀
const uploadDistFilePath = filePath + 'dist.zip' // 本地包地址 需加上文件名和后缀
const serverBuildPath = '/home/xxx/build.zip' //服务器上传地址 需加上文件名和后缀
const serverDistPath = '/home/xxx/dist.zip' //服务器静态资源上传地址 需加上文件名和后缀
const projectBuildPath = '/usr/local/ccc/' //服务器项目运行路径
const projectDistPath = '/xxx/aaa/pc/nuxtstatic/'//服务器静态资源路径
let runCmd = 'npm run build' // 打包命令
// 服务器列表
let serverList = []
// 正式服务器
const proServerList = [
{
name:'static',
cmd: 'su',
shell: 'unzip -o -q ' + serverDistPath + ' -d ' + projectDistPath,
pm2: '',
serverName: '219 静态',
host: '192.168.0.219',
port: '22',
username: 'admin',
password: '123',
rootpass: '456',
serverPath: serverDistPath,
filePath: uploadDistFilePath
},
{
name:'107',
cmd: 'su',
shell: 'unzip -o -q ' + serverBuildPath + ' -d ' + projectBuildPath + ' && cd ' + projectBuildPath + 'build && pm2 delete all && npm run pm_4',
pm2: 'npm run pm_4',
serverName: '107 4节点',
host: '192.168.0.107',
port: '22',
username: 'admin',
password: '123022',
rootpass: '3452022',
serverPath: serverBuildPath,
filePath: uploadBuildFilePath
},
{
name:'181',
cmd: 'su',
shell: 'unzip -o -q ' + serverBuildPath + ' -d ' + projectBuildPath + ' && cd ' + projectBuildPath + 'build && pm2 delete all && npm run pm_4',
pm2: 'npm run pm_4',
serverName: '181 4节点',
host: '192.168.0.181',
port: '22',
username: 'admin',
password: '123022',
rootpass: '1231241',
serverPath: serverBuildPath,
filePath: uploadBuildFilePath
},
{
name: '150',
cmd: 'su',
shell: 'unzip -o -q ' + serverBuildPath + ' -d ' + projectBuildPath + ' && cd ' + projectBuildPath + 'build && pm2 delete all && npm run pm_8',
pm2: 'npm run pm_8',
serverName: '150 8节点',
host: '192.168.0.150',
port: '22',
username: 'admin',
password: 'nsda2021',
rootpass: 'nsdasda022',
serverPath: serverBuildPath,
filePath: uploadBuildFilePath
}
]
// 测试服务器
const devServerList = [
{
cmd: 'su',
shell: 'unzip -o -q /root/build.zip -d /www/wwwroot/ccc/ && cd /www/wwwroot/ccc/build && pm2 delete all && npm run pm2-test',
pm2: 'npm run pm2-test',
serverName: '195 测试',
host: '192.168.0.195',
port: '22',
username: 'root',
password: 'test',
rootpass: 'aaaa',
serverPath: '/root/build.zip',
filePath: uploadBuildFilePath
},
]
// 进度条配置
let config = {
duration: 100,
current: 0,
block: '█',
showNumber: true,
tip: {
0: `打包中...`,
100: `打包完成`
},
color: 'green'
}
let progressBarC = new progressBar(config);
// 获取当前时间
function getNowTime(type) {
let date = new Date()
let year = date.getFullYear()
let month = date.getMonth() + 1
let day = date.getDate()
let hours = date.getHours();
let minute = date.getMinutes();
let second = date.getSeconds();
month = month < 10 ? ('0' + month) : month;
day = day < 10 ? ('0' + day) : day;
hours = hours < 10 ? ('0' + hours) : hours;
minute = minute < 10 ? ('0' + minute) : minute;
second = second < 10 ? ('0' + second) : second;
if (type == 0) {
return `${year}-${month}-${day} ${hours}:${minute}:${second}`
}
if (type == 1) {
return `${year}-${month}-${day}`
}
if (type == 2) {
return `${year}`
}
}
// 执行shell 命令打包项目
function execSyncs(item, cmd, index) {
// console.log(`[${getNowTime(0)}] ${item}`);
const child = spawnSync(cmd, { shell: true, stdio: ['pipe', 'inherit', 'inherit'] });
if (child.status != 0) {
console.log(`[${getNowTime(0)}] ${item}失败`);
} else {
config.tip['0'] = '打包中...'
config.tip['100'] = '打包完成'
progressBarC.run(index * 10)
}
}
// 打包项目
function build() {
const cmdList = {
'打包中(0%)': 'cd ' + itemPath + ' && ' + runCmd + ' && touch -m -a ' + itemPath,
'打包中(10%)': 'cd ' + filePath + ' && rm -rf build build.zip dist dist.zip && mkdir -p build/.nuxt build/static build/node_modules dist',
'打包中(20%)': '(cd ' + itemPath + '.nuxt/ && tar cf - .) | (cd ' + filePath + 'build/.nuxt && tar xpf -)',
'打包中(30%)': '(cd ' + itemPath + 'static/ && tar cf - .) | (cd ' + filePath + 'build/static && tar xpf -)',
'打包中(40%)': '(cd ' + itemPath + 'node_modules/ && tar cf - .) | (cd ' + filePath + 'build/node_modules && tar xpf -)',
'打包中(50%)': 'cp -rf ' + itemPath + 'package.json ' + filePath + 'build',
'打包中(60%)': 'cp -rf ' + itemPath + 'nuxt.config.js ' + filePath + 'build',
'打包中(70%)': 'cp -rf ' + itemPath + 'config.js ' + filePath + 'build',
'打包中(80%)': 'cp -rf ' + itemPath + 'package-lock.json ' + filePath + 'build',
'打包中(90%)': 'cp -rf ' + itemPath + 'ecosystem.config.js ' + filePath + 'build',
'打包中(100%)': '(cd ' + itemPath + '.nuxt/dist/ && tar cf - .) | (cd ' + filePath + 'dist && tar xpf -)', //xpvf
}
try {
Object.keys(cmdList).forEach(function (item, index) {
execSyncs(item, cmdList[item], index)
})
} catch (error) {
console.log(error);
}
}
function Connect(server, then) {
var conn = new Client();
conn.on("ready", function () {
then(conn);
}).on('error', function (err) {
// console.log("connect error!");
}).on('end', function () {
// console.log("connect end!");
}).on('close', function (had_error) {
// console.log("connect close");
}).connect(server);
}
// 上传文件
function UploadFile(server, info, cmd, then) {
let unzip = false
let x = 0
let y = 0
let end = false
config.tip['0'] = `[${info.serverName}]部署中...`
config.tip['100'] = `[${info.serverName}]部署完成`
progressBarC.run(0)
Connect(server, function (conn) {
conn.sftp(function (err, sftp) {
if (err) {
then(err);
} else {
progressBarC.run(5)
// console.log(`[${getNowTime(0)}] 服务器连接成功`);
// console.log(`[${getNowTime(0)}] 开始上传压缩包`);
sftp.fastPut(info.filePath, info.serverPath, function (err, result) {
if (err) console.log(err);
progressBarC.run(20)
// console.log(`[${getNowTime(0)}] 压缩包上传成功`);
// console.log(`[${getNowTime(0)}] 开始部署`);
conn.exec(cmd, { pty: true }, function (err, stream) {
if (err) {
then(err, 0);
} else {// end of if
stream.on('data', async function (data, extended) {
if (data.toString().indexOf('Password:') >= 0) {
progressBarC.run(25)
// console.log(`[${getNowTime(0)}] 切换root`);
stream.write(info.rootpass + '\n');
}
if (data.toString().indexOf('[root@') >= 0) {
if (!unzip) {
progressBarC.run(27)
// console.log(`[${getNowTime(0)}] 登录root成功`);
// console.log(`[${getNowTime(0)}] 开始解压压缩包`);
stream.write(info.shell + '\n');
unzip = true
if (y == 0) {
progressBarC.run(45)
y++
}
}
}
// ...自己的逻辑
}).on('close', async (code, signal) => {
// console.log(`[${getNowTime(0)}] 部署完成`);
then(0, 1)
conn.end();
});
}
});
});
}
});
});
}
// 按序连接服务器 上传文件
function uploadFile(index) {
if (index < serverList.length) {
let item = serverList[index]
let server = {
host: item.host, // 服务器 IP
port: item.port,
username: item.username,
password: item.password
}
UploadFile(server, item, item.cmd, (err, res) => {
if (res > 0) {
index++
uploadFile(index)
} else {
console.log(err);
}
});
}
}
// 压缩文件 zip
function compressFile(targetDir, outputDir, fileName) {
return new Promise((resolve, reject) => {
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); // 如果文件夹不存在,则创建
let output = fs.createWriteStream(`${outputDir}/${fileName}.zip`) // 创建⽂件写⼊流
const archive = archiver('zip', { zlib: { level: 9 } }) // 设置压缩等级
output.on('close', () => {
resolve(' 压缩完成')
}).on('error', (err) => {
reject(console.error(`[${getNowTime(0)}] ${fileName} 压缩失败`, err))
})
let unzip = false
getdirsize(targetDir, function (err, data) {
if (!err) {
config.tip['0'] = `[${fileName}.zip]压缩中...`
config.tip['100'] = `[${fileName}.zip]压缩完成`
archive.on('data', function (param) {
let num = Math.floor((archive._fsEntriesTotalBytes / data) * 100)
if (!unzip) {
progressBarC.run(num)
}
if (num == 100) {
unzip = true
}
})
}
})
archive.pipe(output)
archive.directory(targetDir, fileName) // 存储⽬标⽂件
archive.finalize() // 完成归档
})
}
// 获取文件大小
function getdirsize(dir, callback) {
var size = 0;
fs.stat(dir, function (err, stats) {
if (err) return callback(err);//如果出错
if (stats.isFile()) return callback(null, stats.size);//如果是文件
fs.readdir(dir, function (err, files) {//如果是目录
if (err) return callback(err);//如果遍历目录出错
if (files.length == 0) return callback(null, 0);//如果目录是空的
var count = files.length;//哨兵变量
for (var i = 0; i < files.length; i++) {
getdirsize(path.join(dir, files[i]), function (err, _size) {
if (err) return callback(err);
size += _size;
if (--count <= 0) {//如果目录中所有文件(或目录)都遍历完成
callback(null, size);
}
});
}
});
});
}
async function building() {
// 打包
build()
console.log('');
// 压缩
try {
await compressFile(`${filePath}build`, filePath, 'build')
console.log('');
await compressFile(`${filePath}dist`, filePath, 'dist')
console.log('');
} catch (error) {
console.log(error);
}
}
// 部署
async function deploy() {
// npm run deploy 全部部署 自动打包
// npm run deploy-build 手动打包 // npm run deploy-static 部署静态 不打包
// npm run deploy-107 部署107 不打包
// npm run deploy-181 部署184 不打包
// npm run deploy-150 部署150 不打包
// 全部部署 或 部分部署
serverList = []
if (process.env.ENV_DEPLOY == 'all') {
serverList = proServerList
} else {
let server = proServerList.find(item => {
return item.name == process.env.ENV_DEPLOY
})
serverList.push(server)
}
// 测试环境部署
if (process.env.NODE_ENV == 'dev') {
serverList = devServerList
runCmd = 'npm run build-test'
}
// 是否需要打包,npm run deploy-build
if(process.env.ENV_BUILD == 'true'){
//打包压缩
await building()
}
// 部署到服务器
if (process.env.ENV_DEPLOY != 'no') {
// 服务器部署
uploadFile(0)
}
}
deploy()
大功告成,大家可以根据自己需求自己改动
最后运行一下看下效果npm run deploy-build
(我这里是分步骤执行,也可以直接全部部署)
npm run deploy-static
npm run deploy-107
npm run deploy
全部部署