nodejs版 前端自动化部署
依赖包
text
npm i archiver@^7.0.1 ssh2@1.17.0javascript
// deploy.cjs
const { Client } = require('ssh2');
const path = require('path');
const fs = require('fs');
const archiver = require('archiver');
const readline = require('readline');
// ==================== 配置区域 ====================
// 你可以在这里修改配置
const server = {
host: '47.112.184.104',
port: 22,
username: 'root',
password: 'TWhsuBx14PJp8kLvFlMev3A*^Qd7D',
deployPath: '/usr/local/nginx/html/kzx/a', // 部署目标路径(不要末尾斜杠)
backupSuffix: '_back', // 备份后缀
};
// 本地配置
const localConfig = {
distPath: path.join(__dirname, 'dist'), // 本地构建输出目录
tempZipName: 'deploy-temp.zip', // 临时压缩文件名
};
// ==================== 工具函数 ====================
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const log = {
info: (msg) => console.log('\x1b[36m%s\x1b[0m', `[INFO] ${msg}`),
success: (msg) => console.log('\x1b[32m%s\x1b[0m', `[SUCCESS] ${msg}`),
error: (msg) => console.log('\x1b[31m%s\x1b[0m', `[ERROR] ${msg}`),
warning: (msg) => console.log('\x1b[33m%s\x1b[0m', `[WARNING] ${msg}`),
step: (msg) => console.log('\x1b[34m%s\x1b[0m', `\n➜ ${msg}`)
};
/**
* 获取备份路径
* @param {string} deployPath 部署路径
* @returns {string} 备份路径
*/
function getBackupPath(deployPath) {
return `${deployPath}${server.backupSuffix}`;
}
/**
* 获取部署目录的父路径
* @param {string} deployPath 部署路径
* @returns {string} 父路径
*/
function getParentPath(deployPath) {
return path.posix.dirname(deployPath);
}
/**
* 获取部署目录的文件夹名
* @param {string} deployPath 部署路径
* @returns {string} 文件夹名
*/
function getFolderName(deployPath) {
return path.posix.basename(deployPath);
}
// ==================== 部署函数 ====================
/**
* 检查本地构建目录
*/
async function checkDistFolder() {
log.step('检查本地构建目录...');
if (!fs.existsSync(localConfig.distPath)) {
log.error(`构建目录不存在: ${localConfig.distPath}`);
log.info('请先运行构建命令: npm run build');
return false;
}
const files = fs.readdirSync(localConfig.distPath);
if (files.length === 0) {
log.error('构建目录为空');
return false;
}
log.success(`找到 ${files.length} 个文件/文件夹`);
return true;
}
/**
* 压缩dist目录
*/
async function compressDist() {
log.step('压缩构建文件...');
const zipPath = path.join(__dirname, localConfig.tempZipName);
return new Promise((resolve, reject) => {
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
}
const output = fs.createWriteStream(zipPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => {
const size = (archive.pointer() / 1024 / 1024).toFixed(2);
log.success(`压缩完成: ${size} MB`);
resolve(zipPath);
});
archive.on('error', reject);
archive.pipe(output);
archive.directory(localConfig.distPath, false);
archive.finalize();
});
}
/**
* 测试SSH连接
*/
async function testConnection(conn) {
log.step('测试SSH连接...');
return new Promise((resolve, reject) => {
conn.exec('echo "SSH连接成功" && whoami', (err, stream) => {
if (err) {
reject(err);
return;
}
stream.on('close', (code) => {
if (code === 0) {
log.success('SSH连接正常');
resolve();
} else {
reject(new Error('SSH连接测试失败'));
}
});
stream.on('data', (data) => {
console.log(' 服务器响应:', data.toString().trim());
});
});
});
}
/**
* 检查并准备远程目录
*/
async function prepareRemoteDirectory(conn) {
log.step('准备远程目录...');
const deployPath = server.deployPath;
const backupPath = getBackupPath(deployPath);
const parentPath = getParentPath(deployPath);
const folderName = getFolderName(deployPath);
console.log(` 部署路径: ${deployPath}`);
console.log(` 备份路径: ${backupPath}`);
console.log(` 父级路径: ${parentPath}`);
return new Promise((resolve, reject) => {
const commands = [
`# 创建父目录(如果不存在)`,
`mkdir -p ${parentPath}`,
``,
`# 进入父目录`,
`cd ${parentPath}`,
``,
`# 显示当前目录内容`,
`echo "当前目录内容:"`,
`ls -la`,
``,
`# 处理已存在的部署目录`,
`if [ -d "${folderName}" ]; then`,
` echo "发现已存在的部署目录: ${folderName}"`,
` # 删除旧的备份`,
` rm -rf ${folderName}${server.backupSuffix}`,
` # 将当前目录重命名为备份`,
` mv ${folderName} ${folderName}${server.backupSuffix}`,
` echo "已备份为: ${folderName}${server.backupSuffix}"`,
`fi`,
``,
`# 创建新的部署目录`,
`mkdir -p ${folderName}`,
`echo "创建新的部署目录: ${folderName}"`,
``,
`# 设置目录权限`,
`chmod 755 ${folderName}`,
``,
`# 显示最终结果`,
`echo "目录准备完成:"`,
`ls -la | grep -E "${folderName}|${folderName}${server.backupSuffix}"`
];
const commandString = commands.join('\n');
conn.exec(`bash -c '${commandString.replace(/'/g, "'\\''")}'`, (err, stream) => {
if (err) {
reject(err);
return;
}
let output = '';
stream.on('data', (data) => {
output += data.toString();
process.stdout.write(data.toString());
});
stream.stderr.on('data', (data) => {
process.stderr.write('\x1b[33m' + data.toString() + '\x1b[0m');
});
stream.on('close', (code) => {
if (code === 0) {
if (output.includes('已备份')) {
log.success('已备份原目录并创建新目录');
} else {
log.success('新目录创建完成');
}
resolve();
} else {
reject(new Error(`准备目录失败,退出码: ${code}`));
}
});
});
});
}
/**
* 上传文件到服务器
*/
async function uploadFile(conn, localZipPath) {
log.step('上传文件到服务器...');
const remoteFilePath = path.posix.join(server.deployPath, localConfig.tempZipName);
return new Promise((resolve, reject) => {
conn.sftp((err, sftp) => {
if (err) {
reject(err);
return;
}
const readStream = fs.createReadStream(localZipPath);
const writeStream = sftp.createWriteStream(remoteFilePath);
const totalSize = fs.statSync(localZipPath).size;
let uploadedSize = 0;
let lastPercent = 0;
readStream.on('data', (chunk) => {
uploadedSize += chunk.length;
const percent = Math.floor((uploadedSize / totalSize) * 100);
if (percent > lastPercent) {
lastPercent = percent;
readline.cursorTo(process.stdout, 0);
process.stdout.write(` 上传进度: ${percent}% (${(uploadedSize / 1024 / 1024).toFixed(2)}MB/${(totalSize / 1024 / 1024).toFixed(2)}MB)`);
}
});
writeStream.on('close', () => {
console.log('\n');
log.success('文件上传完成');
resolve();
});
writeStream.on('error', reject);
readStream.pipe(writeStream);
});
});
}
/**
* 在服务器上解压文件
*/
async function extractFiles(conn) {
log.step('解压文件...');
const deployPath = server.deployPath;
const zipFileName = localConfig.tempZipName;
return new Promise((resolve, reject) => {
const commands = [
`cd ${deployPath}`,
`echo "当前目录: $(pwd)"`,
``,
`# 检查压缩文件`,
`if [ ! -f "${zipFileName}" ]; then`,
` echo "错误: 压缩文件不存在"`,
` exit 1`,
`fi`,
``,
`# 显示压缩文件信息`,
`ls -la ${zipFileName}`,
``,
`# 解压文件`,
`echo "正在解压 ${zipFileName} ..."`,
`unzip -o ${zipFileName}`,
`if [ $? -eq 0 ]; then`,
` echo "解压成功"`,
`else`,
` echo "解压失败"`,
` exit 1`,
`fi`,
``,
`# 删除压缩文件`,
`rm -f ${zipFileName}`,
`echo "已删除临时压缩文件"`,
``,
`# 设置文件权限`,
`echo "设置文件权限..."`,
`find . -type d -exec chmod 755 {} \\;`,
`find . -type f -exec chmod 644 {} \\;`,
``,
`# 显示解压后的文件`,
`echo "部署完成,文件列表:"`,
`ls -la | head -15`,
`echo "... (共 $(find . -type f | wc -l) 个文件)"`
];
const commandString = commands.join('\n');
conn.exec(`bash -c '${commandString.replace(/'/g, "'\\''")}'`, (err, stream) => {
if (err) {
reject(err);
return;
}
stream.on('data', (data) => {
process.stdout.write(data.toString());
});
stream.stderr.on('data', (data) => {
process.stderr.write('\x1b[31m' + data.toString() + '\x1b[0m');
});
stream.on('close', (code) => {
if (code === 0) {
log.success('文件解压完成');
resolve();
} else {
reject(new Error('解压失败'));
}
});
});
});
}
/**
* 清理本地临时文件
*/
async function cleanup(localZipPath) {
log.step('清理本地临时文件...');
if (fs.existsSync(localZipPath)) {
fs.unlinkSync(localZipPath);
log.success('本地临时文件已删除');
}
}
/**
* 显示部署信息并确认
*/
async function confirmDeployment() {
console.log('\n📋 部署配置信息:');
console.log(` 🔧 服务器: ${server.host}:${server.port}`);
console.log(` 👤 用户名: ${server.username}`);
console.log(` 📁 部署路径: ${server.deployPath}`);
console.log(` 🔙 备份路径: ${getBackupPath(server.deployPath)}`);
console.log(` 📦 本地目录: ${localConfig.distPath}`);
return new Promise((resolve) => {
rl.question('\n确认部署? (y/N) ', (answer) => {
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
rl.close();
});
});
}
// ==================== 主函数 ====================
async function main() {
console.log('\n\x1b[35m═══════════════════════════════════════\x1b[0m');
console.log('\x1b[35m 前端项目自动化部署工具 v1.0 \x1b[0m');
console.log('\x1b[35m═══════════════════════════════════════\x1b[0m\n');
let conn = null;
let localZipPath = null;
try {
// 1. 确认部署
const confirmed = await confirmDeployment();
if (!confirmed) {
log.info('部署已取消');
process.exit(0);
}
// 2. 检查本地构建目录
const distExists = await checkDistFolder();
if (!distExists) {
throw new Error('请先构建项目');
}
// 3. 压缩本地文件
localZipPath = await compressDist();
// 4. 连接服务器
log.step('连接到服务器...');
conn = new Client();
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('连接超时'));
}, 30000);
conn.on('ready', () => {
clearTimeout(timeout);
log.success('服务器连接成功');
resolve();
}).on('error', (err) => {
clearTimeout(timeout);
reject(err);
}).connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password,
readyTimeout: 20000,
keepaliveInterval: 10000
});
});
// 5. 测试连接
await testConnection(conn);
// 6. 准备远程目录(备份原目录 + 创建新目录)
await prepareRemoteDirectory(conn);
// 7. 上传文件
await uploadFile(conn, localZipPath);
// 8. 解压文件
await extractFiles(conn);
// 9. 清理
await cleanup(localZipPath);
// 10. 完成
log.success('\n✨ 部署成功完成!');
console.log(`\n🌐 访问地址: http://${server.host}${server.deployPath.replace('/usr/local/nginx/html', '')}`);
console.log(`📂 部署目录: ${server.deployPath}`);
console.log(`💾 备份目录: ${getBackupPath(server.deployPath)}\n`);
} catch (error) {
log.error(`部署失败: ${error.message}`);
if (error.stack) {
console.error('\x1b[31m', error.stack, '\x1b[0m');
}
// 清理本地临时文件
if (localZipPath) {
await cleanup(localZipPath);
}
process.exit(1);
} finally {
if (conn) conn.end();
}
}
// 运行主函数
main();