一、实现的最终效果
- 点击一下,即可实现
项目打包
文件压缩(便于上传到服务器)
连接服务器
备份
上传打包的文件
解压
完成部署
二、原理及需要的插件
- 原理: 执行shell脚本
- 需要的npm依赖包:archiver(压缩),ssh2(执行脚本:如连接服务器,解压等操作)
三、详细步骤
3.1 安装包
npm install archiver ssh2 -g
3.2 引入包和编写配置文件
const archiver = require('C:\\Users\\Administrator\\AppData\\Roaming\\npm\\node_modules\\archiver')
const Client = require('C:\\Users\\Administrator\\AppData\\Roaming\\npm\\node_modules\\ssh2').Client
const { exec } = require('child_process')
const path = require('path')
const fs = require('fs')
// 生产环境配置
const productionConfig = {
host: 'xxx.xxx.xxx.xxx', // 服务器ip地址或域名
port: xxx, // 服务器ssh连接端口号
username: 'xxxx', // ssh登录用
password: '****8*', // 密码
catalog: '/opt/apps/xxxxx', // 前端文件压缩目录
}
// 全局配置
const Config = {
Env: productionConfig, // 可填写多个,也可只填写一个
buildDist: 'dist', // 前端文件打包之后的目录,默认dist
buildCommand: 'yarn build', // 打包前端文件的命令
readyTimeout: 20000, // ssh连接超时时间
deleteServerZipFile: true // 是否删除线上上传的dist压缩包
}
3.3 本地文件操作:打包、压缩、删除
class File {
constructor(fileName) {
this.fileName = fileName;
}
// 删除本地文件
deleteLocalFile () {
return new Promise((resolve, reject) => {
fs.unlink(this.fileName, function(error){
if(error){
reject({
success: false,
error
});
} else {
resolve({
success: true
});
}
})
})
}
// 压缩文件夹下的所有文件
zipFile(filePath) {
return new Promise((resolve, reject) => {
// 创建文件输出流
let output = fs.createWriteStream(__dirname + '/' + this.fileName);
let archive = archiver('zip', {
zlib: { level: 9 } // 设置压缩级别
});
// 文件输出流结束
output.on('close', function() {
console.log(`----压缩文件总共 ${archive.pointer()} 字节----`);
console.log('----压缩文件夹完毕----');
resolve({
success: true
})
});
// 数据源是否耗尽
output.on('end', function() {
console.error('----压缩失败,数据源已耗尽----');
reject();
});
// 存档警告
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
console.error('----stat故障和其他非阻塞错误----')
} else {
console.error('----压缩失败----');
}
reject(err);
});
// 存档出错
archive.on('error', function(err) {
console.error('----存档错误,压缩失败----');
console.error(err);
reject(err);
});
// 通过管道方法将输出流存档到文件
archive.pipe(output);
// 打包dist里面的所有文件和目录
archive.directory(filePath, false);
// archive.directory(`../${Config.buildDist}/`, false);
// 完成归档
archive.finalize();
})
}
// 打包本地前端文件
buildProject () {
console.log('----开始编译打包文件,请耐心等待----');
return new Promise((resolve, reject) => {
exec(Config.buildCommand, async (error, stdout, stderr) => {
if (error) {
console.error(error);
reject({
error,
success: false
});
} else if (stdout) {
resolve({
stdout,
success: true
});
} else {
console.error(stderr);
reject({
error: stderr,
success: false
});
}
});
})
}
// 停止程序之前需删除本地压缩包文件
stopProgress() {
this.deleteLocalFile().catch((e)=>{
console.error('----删除本地文件失败,请手动删除----');
console.error(e);
}).then(()=>{
console.log('----已删除本地压缩包文件----');
})
}
}
3.4 SSH连接
/**
* ssh连接
*/
class SSH {
constructor ({ host, port, username, password, agent }) {
this.server = {
host, port, username, password
};
this.conn = new Client();
}
// 连接服务器
connectServer () {
return new Promise((resolve, reject) => {
let conn = this.conn;
conn.on("ready", ()=>{
resolve({
success: true
});
}).on('error', (err)=>{
reject({
success: false,
error: err
});
}).on('end', ()=> {
console.log('----SSH连接已结束----');
}).on('close', (had_error)=>{
console.log('----SSH连接已关闭----');
}).connect(this.server);
})
}
// 上传文件
uploadFile ({ localPath, remotePath }) {
return new Promise((resolve, reject) => {
return this.conn.sftp((err, sftp)=>{
if (err) {
reject({
success: false,
error: err
});
} else{
sftp.fastPut(localPath, remotePath, (err, result)=>{
if (err) {
reject({
success: false,
error: err
});
}
resolve({
success: true,
result
});
});
}
})
})
}
// 执行ssh命令
execSsh (command) {
return new Promise((resolve, reject) => {
return this.conn.exec(command, (err, stream)=>{
if (err || !stream) {
reject({
success: false, error: err
});
} else {
stream.on('close', (code, signal) => {
resolve({
success: true
});
}).on('data', function (data) {
console.log(data.toString());
}).stderr.on('data', function (data) {
resolve({
success: false,
error: data.toString()
});
});
}
});
})
}
// 结束连接
endConn () {
this.conn.end();
}
}
3.5 SSH连接后、上传,解压,删除等相关操作
async function sshUpload (sshConfig, fileName) {
let sshCon = new SSH(sshConfig);
let sshRes = await sshCon.connectServer().catch(e=>{
console.error(e);
});
if (!sshRes || !sshRes.success) {
console.error('----连接服务器失败,请检查用户名密码是否正确以及服务器是否已开启远程连接----');
return false;
}
console.info('----连接成功,开始清除目标文件夹中的内容----')
await sshCon.execSsh(`cd /opt/`).catch((e)=>{});
await sshCon.execSsh(`mkdir ${sshConfig.catalog}`).catch((e)=>{});
await sshCon.execSsh(`mkdir ${sshConfig.catalog}-bak`).catch((e)=>{});
await sshCon.execSsh(`rm -rf ${sshConfig.catalog}-bak/*`).catch((e)=>{});
console.log('----开始备份文件----');
await sshCon.execSsh(`cp -r ${sshConfig.catalog}/* ${sshConfig.catalog}-bak/`).catch((e)=>{});
console.log('----备份文件完成----');
// 注意:rm -rf为危险操作,请勿对此段代码做其他非必须更改
let deleteTargetDir = await sshCon.execSsh(`rm -rf ${sshConfig.catalog + '/*'}`).catch((e)=>{});
if (!deleteTargetDir || !deleteTargetDir.success) {
console.error('----清空失败,请手清空----');
console.error(`----错误原因:${deleteTargetDir.error}----`);
}
console.info('----清除目标文件夹中的内容成功----')
console.log('----开始上传文件----');
let uploadRes = await sshCon.uploadFile({
localPath: path.resolve(__dirname, fileName),
remotePath: sshConfig.catalog + '/' + fileName
}).catch(e=>{
console.error(e);
});
if (!uploadRes || !uploadRes.success) {
console.error('----上传文件失败,请重新上传----');
return false;
}
console.log('----上传文件成功,开始解压文件----');
let zipRes = await sshCon.execSsh(`unzip -o ${sshConfig.catalog + '/' + fileName} -d ${sshConfig.catalog}`)
.catch((e)=>{});
if (!zipRes || !zipRes.success) {
console.error('----解压文件失败,请手动解压zip文件----');
console.error(`----错误原因:${zipRes.error}----`);
}
if (Config.deleteServerZipFile) {
console.log('----解压文件成功,开始删除上传的压缩包----');
// 注意:rm -rf为危险操作,请勿对此段代码做其他非必须更改
let deleteZipRes = await sshCon.execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`).catch((e)=>{});
if (!deleteZipRes || !deleteZipRes.success) {
console.error('----删除文件失败,请手动删除zip文件----');
console.error(`----错误原因:${deleteZipRes.error}----`);
}
}
// 结束ssh连接
sshCon.endConn();
}
3.6 进行最终的整合
(async ()=> {
// 压缩包的名字
let date = new Date();
let year = date.getFullYear();
let month = date.getMonth() + 1;
let day = date.getDate();
let timeStr = `${year}_${month}_${day}`;
const fileName = `${Config.buildDist}-`+ timeStr + '-' + Math.random().toString(16).slice(2) + '.zip';
let file = new File(fileName);
// 打包文件
let buildRes = await file.buildProject().catch(e=>{
console.error(e);
});
if (!buildRes || !buildRes.success) {
console.error('----编译打包文件出错----');
return false;
}
console.log(buildRes.stdout);
console.log('----编译打包文件完成----');
// 压缩文件
let res = await file.zipFile(`${Config.buildDist}/`).catch(()=>{});
if (!res || !res.success) return false;
console.log('----开始进行SSH连接----');
if (Config.publishEnv instanceof Array && Config.publishEnv.length) {
for (let i = 0; i < Config.publishEnv.length; i++) {
await sshUpload(Config.publishEnv[i], fileName);
}
} else {
await sshUpload(Config.publishEnv, fileName);
}
console.log('----部署成功,正在为您删除本地压缩包----');
file.stopProgress();
})();
四、把上面所有代码放在一个文件就OK,接下来配置就ok,参考代码
[百度网盘](链接:https://pan.baidu.com/s/1ZvqOfxIDeevf1cmsfjqEcg
提取码:3mzt
–来自百度网盘超级会员V4的分享)
五、配置一键自动部署脚本