公告

👇👇👇扫我

图片
Skip to content

nodejs版 前端自动化部署

依赖包

text
npm i archiver@^7.0.1 ssh2@1.17.0
javascript
// 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();
阅读量: 0
评论量: 0