记录前端菜鸟的日常——自动打包脚本

实习和工作我用过的前端包上传到测试就两种方法:FTP/SFTP和Jenkins(也就是CI/CD自动部署),目前一直是手动拖dist包,最近感觉比较麻烦,让ai帮我写了个脚本去部署。

一、FTP、SFTP

手动拖dist包到服务器上,FTP是文件传输协议、SFTP是基于SSH的更安全的文件传输协议,我用的工具是FileZilla,还可以用WinSCP。输入服务器IP和用户名密码连接之后就把dist包手动拖进去,之后刷新页面就ok了

二、CI/CD自动部署

CI = Continuous Integration(持续集成):代码 push 后自动构建、测试

CD = Continuous Deployment(持续部署):构建成功后自动发布到服务器

需要在项目里创建CI文件,告诉系统什么时候打包(push到哪个分支)、几点上传、怎么打包和上传

三、脚本化部署

我们现在用的是第一种,手动拖到FileZilla里,时间长了我觉得有点麻烦,然后让ai帮我写了个脚本,我的想法是创建一个中间文件夹,新的包打好之后先放这个新文件夹里,然后把当前项目里的dist包命名为dist_时间戳的,再把新的包拿出来

新加了这两个命令:

复制代码
"deploy:sit": "node deploy/deploy.js sit",
"deploy:uat": "node deploy/deploy.js uat",

在项目根目录下创建deploy文件夹(注意不要放在src里,src里是会被打包上去的)

deploy.js:

复制代码
const { exec } = require('child_process');
const path = require('path');
const fs = require('fs');
const readline = require('readline');

// ============================================================
// 使用方式:
//   node deploy/deploy.js sit      → 部署到 SIT 环境
//   node deploy/deploy.js          → 交互式选择环境
// ============================================================

// 获取环境参数
let env = process.argv[2];

// 如果没有指定环境,交互式选择
if (!env) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  
  console.log('\n 请选择部署环境:');
  console.log('  1. sit   (SIT 测试环境)');
  console.log('  2. uat   (UAT 测试环境)');
  console.log('  3. 退出');
  
  rl.question('\n请输入编号 (1-3): ', (answer) => {
    const map = { '1': 'sit', '2': 'uat' };
    if (map[answer]) {
      rl.close();
      deploy(map[answer]);
    } else {
      console.log('已退出');
      rl.close();
    }
  });
} else {
  deploy(env);
}

// ============================================================
// 部署主函数
// ============================================================
function deploy(env) {
  console.log(`\n 开始部署到 ${env} 环境...\n`);
  
  // 1. 加载配置文件
  const configPath = path.join(__dirname, `config.${env}.js`);
  if (!fs.existsSync(configPath)) {
    console.error(`配置文件不存在: ${configPath}`);
    console.log('请先创建 deploy/config.' + env + '.js 文件');
    process.exit(1);
  }
  
  const config = require(configPath);
  console.log(`环境: ${config.env}`);
  console.log(`服务器: ${config.server.host}`);
  console.log(`应用路径: ${config.remotePath}`);
  console.log(`打包命令: ${config.buildCommand}`);
  console.log('');
  
  // 2. 确认部署
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  
  rl.question(`确认部署到 ${env} 环境?(y/N) `, (answer) => {
    rl.close();
    if (!/^[Yy]$/.test(answer)) {
      console.log('已取消部署');
      process.exit(0);
    }
    // 3. 检查服务器连接
    checkServer(config);
  });
}

// ============================================================
// 检查服务器连接
// ============================================================
function checkServer(config) {
  console.log(`\n 检查服务器连接 ${config.server.host}...`);
  
  const checkCmd = `ssh -p ${config.server.port} ${config.server.username}@${config.server.host} "echo '连接成功'"`;
  
  exec(checkCmd, (err, stdout) => {
    if (err) {
      console.error('无法连接服务器');
      console.error('请确认:');
      console.error('  1. 已连接内网 VPN');
      console.error(`  2. 可以 ping 通 ${config.server.host}`);
      console.error(`  3. SSH 端口 ${config.server.port} 是否开放`);
      process.exit(1);
    }
    console.log(` ${stdout.trim()}`);
    doBuild(config);
  });
}

// ============================================================
// 打包
// ============================================================
function doBuild(config) {
  console.log(`\n 正在打包 (${config.buildCommand})...`);
  
  const build = exec(config.buildCommand, { 
    cwd: path.join(__dirname, '..') 
  });
  
  build.stdout.on('data', (data) => {
    console.log(data);
  });
  
  build.stderr.on('data', (data) => {
    console.error(data);
  });
  
  build.on('close', (code) => {
    if (code !== 0) {
      console.error(`打包失败,退出码: ${code}`);
      process.exit(1);
    }
    
    // 检查 dist 是否存在
    const distPath = path.join(__dirname, '../dist');
    if (!fs.existsSync(distPath)) {
      console.error('dist 文件夹不存在,打包可能有问题');
      process.exit(1);
    }
    
    const distSize = fs.statSync(distPath);
    if (distSize.size === 0) {
      console.error('dist 文件夹为空');
      process.exit(1);
    }
    
    console.log('打包完成');
    doUpload(config);
  });
}

// ============================================================
// 上传
// ============================================================
function doUpload(config) {
  const releaseDistPath = `${config.remotePath}/releases/dist`;
  
  console.log(`\n 上传到 ${releaseDistPath}...`);
  
  // 先确保 releases/dist 目录存在(使用 --delete 会清空旧内容)
  const mkdirCmd = `ssh -p ${config.server.port} ${config.server.username}@${config.server.host} "mkdir -p ${releaseDistPath}"`;
  
  exec(mkdirCmd, (err) => {
    if (err) {
      console.error('创建 releases/dist 目录失败');
      process.exit(1);
    }
    
    // 上传到 releases/dist
    const rsyncCmd = `rsync -avz --delete -e "ssh -p ${config.server.port}" ./dist/ ${config.server.username}@${config.server.host}:${releaseDistPath}/`;
    
    const rsync = exec(rsyncCmd, { cwd: path.join(__dirname, '..') });
    
    rsync.stdout.on('data', (data) => console.log(data));
    rsync.stderr.on('data', (data) => console.error(data));
    
    rsync.on('close', (code) => {
      if (code !== 0) {
        console.error(`上传失败,退出码: ${code}`);
        process.exit(1);
      }
      console.log('上传完成');
      doSwitchVersion(config);
    });
  });
}

// ============================================================
// 切换版本(重命名老版本 + 移出新版本)
// ============================================================
function doSwitchVersion(config) {
  const now = new Date();
  const dateStr = 
    String(now.getFullYear()) +
    String(now.getMonth() + 1).padStart(2, '0') +
    String(now.getDate()).padStart(2, '0') +
    '_' +
    String(now.getHours()).padStart(2, '0') +
    String(now.getMinutes()).padStart(2, '0');
  
  const appPath = config.remotePath;
  const oldDistPath = `${appPath}/dist`;
  const backupPath = `${appPath}/dist_${dateStr}`;
  const newDistPath = `${appPath}/releases/dist`;
  
  console.log(`\n 切换版本...`);
  console.log(`   老版本备份为: dist_${dateStr}`);
  console.log(`   新版本从: releases/dist -> dist`);
  
  // 原子操作:重命名老版本 → 移出新版本
  const switchCmd = `ssh -p ${config.server.port} ${config.server.username}@${config.server.host} "
    if [ -d ${oldDistPath} ]; then
      echo '备份老版本...' && mv ${oldDistPath} ${backupPath}
    else
      echo '首次部署,无需备份'
    fi &&
    echo '部署新版本...' && mv ${newDistPath} ${oldDistPath} &&
    echo '切换完成'
  "`;
  
  exec(switchCmd, (err, stdout, stderr) => {
    if (err) {
      console.error('切换版本失败');
      console.error(stderr);
      process.exit(1);
    }
    
    console.log(stdout);
    console.log(`\n 部署完成!`);
    console.log(`当前版本: ${oldDistPath}`);
    console.log(`备份版本: ${backupPath}`);
    console.log('\n 全部完成!');
  });
}

其实命令也可以不带具体环境,这里做了容错可以手动输入,我嫌麻烦直接写死了

同目录下的config.sit.js:

复制代码
module.exports = {
  env: 'sit',
  server: {
    host: 'xxxx',   // 内网开发服务器
    username: 'xxx',
    password: 'xxxx',
    port: xx,
  },
  remotePath: 'xxxx',
  buildCommand: 'node ./addVersion.js && vue-cli-service build --mode sit',
//这里是打包命令,所以这个命令是打包+部署
};

上传成功之后可以去服务器上检查一下是否多了release文件夹和一个带时间戳的dist旧包

相关推荐
CodeStats1 小时前
【Java进程通信】Java进程通信系统完全指南:从ProcessBuilder底层原理到多语言实战
java·前端·python·进程·ai编程·processbuilder
鸿蒙开发1 小时前
鸿蒙 ArkTS 校验库 @hmkit/validator 0.5.0:国际化、错误码和 Schema 组合都来了
前端
wenruozhu1 小时前
【面试题解】 Vue 响应式原理
前端·面试
01_ice1 小时前
前端学习JS(3)
前端·javascript·学习
无懈可击1 小时前
Vue 低代码可视化 AI 表单设计器 FcDesigner v3.5 版本发布!
前端·vue.js·前端框架
哈__1 小时前
极空间部署 Spug:集中管理主机、批量执行脚本与 Web 终端
前端
Virony1 小时前
React基础知识
前端·react.js·前端框架
Hilaku1 小时前
外包前端和大厂前端,写的代码到底有什么本质区别?
前端·javascript·程序员
liangshanbo12152 小时前
React Hooks 为什么不能在条件分支/循环中调用?
前端·react.js·前端框架