前端项目自动部署

一、实现的最终效果

  • 点击一下,即可实现
    项目打包
    文件压缩(便于上传到服务器)
    连接服务器
    备份
    上传打包的文件
    解压
    完成部署

二、原理及需要的插件

  • 原理: 执行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的分享)

五、配置一键自动部署脚本

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、如果你感觉有用的话,给个赞再走吧

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值