# 基于 node 的自动发布方案

背景 :在项目开发提测阶段,由于需要及时的回归测试 bug,可能需要频繁的发布测试版本;因为目前公司jenkins环境还没有完善,所以每次发布还都是手工打包,然后通过 ftp 上传打包后的静态文件;整个过程无聊,且没有一点技术含量; 所以就萌生了写一个简单的自动发布脚本年头;

基本思路:看一张图

流程图

大概的思路就是这样:

第一步:首先我们来看一下 npm run dep 命令;这是一个我们自定义的命令,该内容只要作用就是执行项目根目录下的 deploy.sh shell 脚本文件;

"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "dep"  : "bash deploy.sh"
},

第二步git 提交,推送;当然 git 的提交和推送这种苦力活我们肯定是要避免的;就让把它交给咱们的脚本来执行吧;

echo "开始执行git add ..."

git add -A

# 填写commit 信息
message=""

echo -n "请填写commit信息:"

while IFS= read -r -s -n1 char
do
  # 如果读入的字符为空,则退出 while 循环
  if [ -z $char ]
  then
    echo
    break
  fi
  # 如果输入的是退格或删除键,则移除一个字符
  if [[ $char == $'\x08' || $char == $'\x7f' ]]
  then
    [[ -n $message ]] && message=${message:0:${#message}-1}
    printf '\b \b'
  else
    message+=$char
    printf $char
  fi
done

# 提交代码
git commit -m $message

# 推送代码至远程分支
git push

第三步: 判断当前是否为 test 分支

# 获取当前分支
branch=`git branch | grep \* | cut -d ' ' -f2`
# 判断当前分支是否是 test 分支
# 是 --> 执行是否构建项目函数(isBuild)
if [ $branch == "test" ]; then
  isBuild
fi

第四步:是否构建项目

# 是否需要构建项目函数
function isBuild () {
  read -p "是否需要构建项目?[Y/N]" build
  case $build in
    [yY]*)
      # 执行测试环境打包操作
      npm run build:test
      ;;
    [nN]*)
      exit
      ;;
    *)
      echo "仅能输入[yY]或者[nN]"
      isBuild
      ;;
  esac
}

第五步: 是否需要发布项目;在项目构建完成之后询问是否需要发布项目;但是问题是如何监听项目是否构建完成?

在脚本中我们无法知道项目是否构建完成,我们只能从项目本身入手;本项目采用的是 vue-cli3 搭建;先分享一个 vue-cli3 中如何监听项目是否构建完成的方法;

讲到这里其实我们首先想到的应该就是 webpack plugin ; 在webpack插件中的 apply 方法会被 webpack compiler 调用,接受一个 compiler 对象;通过这个对象来访问 整个编译的生命周期;既然如此,那么一定可以监听到项目打包完成,通过监听项目构建完成的钩子来执行我们自定义的函数去做项目发布动作;

一起来看一下一个简单的webpack插件是如何工作的:

const publish = require('./publish.js')
module.exports = {
  configureWebpack: {
    plugins: [
      {
        apply: compiler => {
          // 简体构建完成的钩子 done;
          // "publishPlugins" 为插件插件名称,自定义
          compiler.hooks.done.tap('publishPlugins', () => {
            // 执行自定义操作,
            // 因为publish.js中要读取dist 目录,如果直接调publish方法,dist目录可能还未生成,所以在这里设置了一个异步,等待dist目录生成;
            setTimeout(() => {
              publish()
            })
          })
        }
      }
    ]
  }
}

接下来我们看看 publish.js 中到底是如何发布的;

第六步 读取打包出来的静态文件

没什么可讲的,都是一些 node 的工具库;这些库不需要额外的安装,webpack 项目中都已经安装过的,直接使用;直接看代码;

const glob = require('glob')
const async = require('async')
const path = require('path')
// 获取dist目录下所有的文件
// **/** 表示深层递归dist目录下所有的文件和目录
const distPath = path.join(__dirname, './dist/**/**')
// 获取所有文件路径,生成一个以为数组,且按顺序排列
const files = glob.sync(distPath)
// async.eachSeries 用以遍历数组,且执行操作,当上一次操作执行完成,callback被调用后开始执行下一次循环
async.eachSeries(files, (file, callback) => {})

第七步 利用 ssh2sftp 对象上传文件

首先大家需要安装一个依赖 -- ssh2; 这是一个 node 的 js 库,用来登录,操作远程服务器;功能非常的强大;有兴趣的可以深入了解一下;

const ssh = require('ssh2')
const Client = ssh.Client
const conn = new Client()

// 连接远程服务器
conn
  .on('ready', () => {
    // 连接成功
  })
  .on('error', () => {
    // 连接失败
  })
  .connect({
    host: '127.0.0.1', // 服务器ip
    port: 22, // 端口号
    username: 'root', // 登录用户名
    password: '123456' // 密码
  })

服务器连接成功之后我们该做的就是将打包好的文件上传到服务器前端部署目录,此时我们需要利用 ssh2sftp 对象来帮助我们完成文件上传;

const remotePath = '/opt/dist/feProject'
const localPath = path.join(__dirname, './dist')

conn
  .on('ready', () => {
    // 连接成功
    conn.sftp((err, sftp) => {
      if (err) {
        console.log('sftp 实例化失败')
        return
      }

      // 开始利用 sftp 上传文件
      // sftp.stat 来判断远程目录地址是否存在
      sftp.stat(remotePath, async (e) => {
        // 不存在,创建远程目录
        if (e) {
          await sftp.mkdir(remotePath)
        }
        // 存在,直接上传文件
        uploadFiles(sftp)
      })
    })
  })

// 判断当前操作系统,切换路径
const unixy = function (filepath){
  if (process.platform === 'win32') {
      return filepath.replace(/\\/g, '/')
  }
  return filepath
}

const createRemoteDir = (sftp, dirPath, callback) => {
  // 判断远程目录地址是否存在
  sftp.stat(dirPath, (err) => {
    if (err) {
      sftp.mkdir(dirPath, (e) => {
        callback(e)
      })
    } else {
      callback()
    }
  })
}

const uploadFileToRemote = (sftp, file, filePath, callback) => {
  sftp.fastPut(file, filePath, (e) => {
    if (e) {
      console.log(e)
    }
    callback(e)
  })
}

function uploadFiles (sftp) {
  const distPath = path.join(__dirname, './dist/**/**')
  let files = glob.sync(distPath)
  async.eachSeries(files, (file, callback) => {
    let distPath = unixy(localPath)
    // 获取文件夹的绝对路径
    let absolutePath = file.replace(distPath, '')
    let realRemotePath = remotePath + absolutePath
    let stat = fs.statSync(file)
    // 判断当前是文件夹还是文件
    if (stat.isDirectory()) {
      // 创建远程目录文件夹
      createRemoteDir(sftp, realRemotePath, callback)
    } else {
      // 文件直接上传
      uploadFileToRemote(sftp, file, realRemotePath, callback)
    }
  })

流程结束