轻量化前端更新方案

1,507 阅读4分钟

前言

​ 一句话介绍:它是可以一行命令将代码更新到服务器的脚本

​ 轻量级更新方案最开始源于掘金的文章,后来我们从零实现了一个更新脚本,并且已在是生产环境中进行使用很长时间,算是非常稳定的版本,个人认为轻量化更新方案是非常使用小型开发团队

​ 现在我们切换到了gitlab的CI/CD。所以这种方案已经不再是我们的主流方案,但是一路使用过来,非常稳定的解决了更新问题,还是非常不错的

优点: 快速,稳定,自动备份指定文件夹(灵活性高,但是需要自己实现)

缺点:需要手动回滚(自动回滚需要编码),相对来说没那么规范,没有留下记录,敏感数据存储在电脑中,配置文件可以git忽略

核心流程

  1. 确认并打包项目
  2. 通过node-ssh连接线上服务器
  3. 将打包代码指定名称进行压缩
  4. 备份之前的代码,删除以前的代码包,并解压压缩包
  5. 删除本次打包代码,断开ssh链接

如何使用

代码地址

  1. 将仓库文件放入项目

  2. 安装依赖

npm install node-ssh inquirer archiver -D
  1. 修改upload.config.js内容
  2. 增加脚本命令
"upload": "node build/upload.js"
  1. 运行命名,验证是否功能正常

核心代码

build/upload.js

const fs = require('fs')
const path = require('path')
const { NodeSSH } = require('node-ssh')
const archiver = require('archiver')
const inquirer = require('inquirer')
const exec = require('child_process').exec
const ssh = new NodeSSH()
const uploadFun = require('../upload.js')

/**
 * 获取当前平台
 */
let objName = process.argv[2] // 更新名字
let startTime = null // 程序开始更新的时间
// 获取上传服务器配置
let config = uploadFun(objName)
const verifyList = [
  {
    type: 'input',
    message: '您正在更新到线上环境,请确认接口域名',
    name: 'objName',
  },
]
inquirer.prompt(verifyList).then(() => {
  uploadBuild()
})

function uploadBuild() {
  startTime = new Date()
  console.log(`${objName}开始更新`)
  let buildcmd = exec(config.buildScript, (error, stdout, stderr) => {
    if (!error) {
      console.log('打包完成', stdout)
      app()
    } else {
      console.error('打包出现错误', stderr)
      process.exit(0)
    }
  })
  buildcmd.stdout.on('data', (data) => {
    console.log(data.toString())
  })
}

/**
 * 通过ssh链接服务器
 */
function app() {
  ssh
    .connect({
      host: config.host,
      username: config.username,
      password: config.password,
    })
    .then((res) => {
      // 上传代码压缩包
      uploadData()
    })
    .catch((err) => {
      console.log(err)
    })
}

/**
 * 上传代码 压缩现有代码
 */
function uploadData() {
  // 创建文件输出流
  let output = fs.createWriteStream(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`)
  // 设置压缩级别
  let archive = archiver('zip', {
    zlib: {
      level: 8,
    },
  })
  // 存档警告
  archive.on('warning', function(err) {
    if (err.code === 'ENOENT') {
      console.warn('stat故障和其他非阻塞错误')
    } else {
      throw err
    }
  })
  // 存档出错
  archive.on('error', function(err) {
    throw err
  })
  // 通过管道方法将输出流存档到文件
  archive.pipe(output)
  archive.directory(`${path.join(__dirname, '../')}${config.buildPath}/${config.buildobj}`, '/')
  archive.finalize()
  // 文件输出流结束
  output.on('close', function() {
    console.log(`总共 ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB,完成源代码压缩`)
    ssh
      .putFile(
        `${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`,
        `${config.uploadDir}/${config.objname}.zip`
      )
      .then(() => {
        console.log('程序zip上传成功,判断线上是否需要备份')
        runcmd()
      })
      .catch((err) => {
        console.log(err)
      })
  })
}

/**
 * 执行ssh命令 判断当前是否存在备份
 */
function runcmd() {
  ssh
    .execCommand('ls', {
      cwd: config.uploadDir,
    })
    .then((res) => {
      if (res.stdout) {
        let fileList = res.stdout.split('\n')
        if (config.objname == config.backObject) {
          if (fileList.includes(config.objname)) {
            console.log('当前更新为线上正常环境,开始进行备份')
            backupData()
          } else {
            console.log('当前更新为线上正常环境,并且是第一次,将跳过备份')
            cmdunzip()
          }
        } else {
          console.log('当前为测试环境,无需备份,直接解压上传压缩包')
          cmdunzip()
        }
      } else if (res.stderr) {
        console.log('查询指定目录失败')
      } else {
        console.log('ssh链接发生了错误')
      }
    })
}

/**
 * 备份项目
 */
function backupData() {
  ssh
    .execCommand(`mv ${config.objname} backup/${config.objname}_backup${new Date().getTime()}`, {
      cwd: config.uploadDir,
    })
    .then((res) => {
      if (res.stderr) {
        console.log('备份发生错误', res.stderr)
      } else {
        console.log('完成备份,解压最新代码')
        cmdunzip()
      }
    })
    .catch((err) => {
      console.log('备份发生未知链接错误', err)
    })
}

/**
 * 解压最新代码zip
 */
function cmdunzip() {
  // 解压程序
  ssh
    .execCommand(
      `rm -rf ${config.objname} && unzip -o -d ${config.uploadDir}/${config.objname} ${config.objname}.zip  && rm -f ${config.objname}.zip`,
      {
        cwd: config.uploadDir,
      }
    )
    .then(() => {
      console.log(`项目包完成解压,${config.objname}项目部署成功了!`)
      console.log(`项目更新时长${(new Date().getTime() - startTime.getTime()) / 1000}s`)
      return deletelocalFile().then(() => {
        console.log('本地缓存zip清除完毕')
      })
    })
    .then(() => {
      ssh
        .execCommand(`rm -rf ${config.objname}/static/.DS_Store`, {
          cwd: config.uploadDir,
        })
        .then(() => {
          console.log('线上项目.DS_Store删除完成')
          ssh.dispose()
          process.exit(0)
        })
        .catch((err) => {
          console.log(err)
        })
    })

    .catch((err) => {
      console.log('解压出现错误', err)
    })
}
/**
 *删除本地生成的压缩包
 */
function deletelocalFile() {
  return new Promise((resolve, reject) => {
    fs.unlink(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`, (err) => {
      if (err) {
        reject(err)
        throw err
      } else {
        resolve()
      }
    })
  })
}

配置文件

upload.config.js

// 打包核心配置文件
let Available = ['dist-a', 'dist-b'] // dist-a 环境a代码包 dist-b 环境b代码包 npm run upload dist-a
/**
 * 获取更新配置
 * @param {String} objName 当前更新名称
 * @returns
 */
module.exports = (objName) => {
  if (!Available.includes(objName)) {
    console.log('当前项目不存在您输入的更新命令,请检查更新名称')
    process.exit(0)
  }
  return {
    host: 'xx.xx.xx.xx', // 服务器地址
    username: 'root',
    password: 'xxxxxxxxxx',
    buildPath: '', // 本地打包项目地址(多层路径用这个)
    buildobj: 'dist', // 本地打包文件名称
    uploadDir: '/xx/xx/xx', // 服务端项目地址
    objname: objName, // 打包项目名称
    backObject: 'objName', // 备份的文件夹名称
    buildScript: 'npm run build' // 更新命令
  }
}

触发命令

最后在package.json增加一行命令,运行前面的脚本文件

 "scripts": {
    // ......
    "upload": "node build/upload.js"
  },

依赖版本

​ 因为更新脚本是在项目里面的,所以需要额外安装依赖

推荐版本号

 "node-ssh": "^12.0.0",
 "inquirer": "^7.3.3",
 "archiver": "^3.1.1",

实际使用

npm run upload xxxx // 线上代码文件夹名称

upload 命令后面的字符串就是服务器上的文件夹名称,这里为了防止更新命名敲错了,需要首先在upload.config.js中进行更新白名单声明,如果配置都正确的情况下,你就可以看到,这就代表成功了~

最后

​ 脚本文件还存在很高的上限,可以优化一下备份部分的备份代码生成规则,再增加一个回滚代码的脚本,就可以实现线上的无感知回滚了

​ 如果使用中遇到了什么问题,请到QQ群 530496237,一起吹吹水