# 自动化部署

大部份公司应该都使用了 Jenkins 部署项目,这里主要是讲如果没有 Jenkins 的话前端自己使用 node 实现一个自动化部署脚本

# 自动化部署脚本

deploy-cli-service (opens new window) 是目前找到的一个前端自动化部署 Node 脚本。

但是实际使用的时候并不能完全部门的发布要求,所以就自己简单实现了一个

先捋下这个自动化部署脚本应该需要实现的基本功能

  1. 压缩需要部署的文件

  2. 连接服务器

  3. 上传压缩文件

  4. 解压文件

压缩需要部署的文件

node可以使用 archiver (opens new window) 模块实现压缩功能

const archiver =require('archiver');

//压缩dist目录为public.zip
function startZip() {
  console.log('开始压缩dist目录...');
  const archive = archiver('zip', {
    zlib: { level: 9 } //递归扫描最多5层
  })
    .on('error', function(err) {
      console.log('压缩失败')
      throw err;//压缩过程中如果有错误则抛出
    });
  const output = fs.createWriteStream(__dirname + '/public.zip')
    .on('close', function(err) {
      /*压缩结束时会触发close事件,然后才能开始上传,
        否则会上传一个内容不全且无法使用的zip包*/
      if (err) {
        console.log('关闭archiver异常:',err);
        return;
      }
      console.log('已生成zip包');
      uploadFile();
    });

  archive.pipe(output);// 压缩内容输出到 zip
  archive.directory(path.resolve(__dirname,'./dist'), false); // 压缩内容直接放在zip包的根目录中
  // archive.directory(srcPath, '/public');// 压缩内容放在zip包目录中 '/public' 路径中
  archive.finalize(); // 执行打包
}

以上就实现了将当前目录中 dist 文件夹压缩成名叫 public.zip 压缩包

连接服务器

连接服务器可以使用 node-ssh (opens new window) 模块

  const { NodeSSH } = require('node-ssh')
  const ssh = new NodeSSH();
  ssh.connect({
    host: config.host,
    port: config.port,
    username: config.username,
    password: config.password,
  }).then(function () {
    console.log('ssh连接成功...');
  }).catch(err=>{
    console.log('ssh连接失败:',err);
  });

以上代码就实现了 ssh 的连接

上传文件

    // localPublishZipPath 本地路径,如  __dirname + '/public.zip'
    // remotePublishZipPath 远程路径, 如 config.publishPath + '/public.zip'
    ssh.putFile(localPublishZipPath, remotePublishZipPath).then(function(status) {
      console.log('上传文件成功');
    }).catch(err=>{
      console.log('文件传输异常:',err);
    });

解压文件

解压文件就是在服务端执行 shell 命令

// remotePublishZipPath 要解压的压缩包路径 如 config.publishPath + '/public.zip'
// remotePublishPath 解压后的文件路径 如 config.publishPath + '/public'
ssh.execCommand(`unzip -o ${remotePublishZipPath} -d ${remotePublishPath}`)
    .then(() => {
      console.log('解压成功')
    })
    .catch(err => {
      console.log('解压失败')
    })

一般解压文件需要文件前需要删除之前项目文件,可以使用以下命令

ssh.execCommand(`/bin/rm -rf ${remotePublishPath}`)
    .then(() => {
      console.log('删除远程文件成功')
    })
    .catch(err => {
      console.log('删除远程文件失败')
    })

有了这些部分基本就实现自动化部署,然后实际工作使用中就可以根据自己需要添加配置扩展

注意点,执行成功或者捕获到失败时应该执行 process.exit 主动退出当前 node 进程

完整例子:

const path = require('path');
const archiver =require('archiver');
const fs = require('fs');
const { NodeSSH } = require('node-ssh')
const ssh = new NodeSSH();
const config = require('./deploy.config');

const srcPath = path.resolve(__dirname, config.distPath); // 要上传的文件
if(!fs.existsSync(srcPath)){
  console.log(`不存在${config.distPath}目录`)
  return
}
const localZipPath = path.resolve(__dirname, `${config.distPath}.zip`); // 压缩名件名
let remotePublishZipPath = path.resolve(config.publishPath, `${config.distPath}.zip`)
let remotePublishPath = path.resolve(config.publishPath, `/${config.distPath}`)

//执行远端部署脚本
async function doJob() {
  try {
    await startZip() // 压缩
    await connectSSH() // 连接 ssh
    await uploadFile() // 上传
    await removeRemoteFile() // 移除原来的目录
    await unzipRemoteFile() // 解压文件
    if(config.isRemoveRemoteZip){
      await removeRemoteZip() // 移除上传的压缩包
    }
    if(config.isRemoveLocalZip){
      await removeLocalZip() // 移除本地的压缩包
    }
  }catch (err){
    console.log(err)
  }finally {
    process.exit(0);
  }
}
//压缩文件
function startZip() {
  console.log('开始压缩目录...');
  return new Promise((resolve, reject) => {
    const archive = archiver('zip', {
      zlib: { level: 9 } //递归扫描最多5层
    })
      .on('error', function(err) {
        console.log('压缩失败')
        reject(err);//压缩过程中如果有错误则抛出
      });
    const output = fs.createWriteStream(localZipPath)
      .on('close', function(err) {
        /*压缩结束时会触发close事件,然后才能开始上传,
          否则会上传一个内容不全且无法使用的zip包*/
        if (err) {
          console.log('关闭archiver异常:',err);
          reject(err)
          return;
        }
        console.log('已生成zip包');
        resolve();
      });

    archive.pipe(output);// 压缩内容输出到 zip
    archive.directory(srcPath, false); // 压缩内容直接放在zip包的根目录中
    // archive.directory(srcPath, '/public');// 压缩内容放在zip包目录中 '/public' 路径中
    archive.finalize(); // 执行打包
  })
}
// 上传文件
function connectSSH(){
  console.log('开始ssh连接...');
  return ssh.connect({
    host: config.host,
    port: config.port,
    username: config.username,
    password: config.password,
  }).then(function () {
    console.log('ssh连接成功...');
  }).catch(err=>{
    console.log('ssh连接失败:',err);
    throw err
  });
}
// 上传文件
function uploadFile(){
  console.log('上传本地压缩文件');
  //上传网站的发布包至configs中配置的远程服务器的指定地址
  return ssh.putFile(localZipPath, remotePublishZipPath).then(function(status) {
    console.log('上传文件成功');
  }).catch(err=>{
    console.log('文件传输异常:',err);
    throw err
  });
}
// 解压上传的文件
function unzipRemoteFile(){
  console.log('开始解压文件:', remotePublishZipPath)
  return ssh.execCommand(`unzip -o ${remotePublishZipPath} -d ${remotePublishPath}`)
    .then(() => {
      console.log('解压成功')
    })
    .catch(err => {
      console.log('解压失败')
      throw err
    })
}
function removeRemoteFile(){
  console.log('删除远程文件:'+remotePublishPath)
  // 货拉拉需要使用 /bin/rm 删除文件
  return ssh.execCommand(`/bin/rm -rf ${remotePublishPath}`)
    .then(() => {
      console.log('删除远程文件成功')
    })
    .catch(err => {
      console.log('删除远程文件失败')
      throw err
    })
}
function removeRemoteZip(){
  console.log('删除远程压缩文件:'+remotePublishZipPath)
  // 货拉拉需要使用 /bin/rm 删除文件
  return ssh.execCommand(`/bin/rm -rf ${remotePublishZipPath}`)
    .then(() => {
      console.log('删除远程压缩文件成功')
    })
    .catch(err => {
      console.log('删除远程压缩文件失败')
      throw err
    })
}
function removeLocalZip(){
  console.log('删除本地压缩文件:'+localZipPath)
  fs.unlinkSync(localZipPath)
  console.log('成功删除本地压缩文件')
}
doJob();

完整代码 (opens new window)

# GitHub Actions

通过 GitHub Actions (opens new window) 实现了静态博客的自动化部署

比如本人使用 VuePress 静态网站生成器制作博客。博客源码地址在 Github 仓库 Hello-Word (opens new window)。构建后的文件分别部署在 Github 仓库 blog (opens new window) 和 Gitee 仓库 lanjz (opens new window) 两个地方。如果使用手动的方法,每次更新完博客后的打包步骤为:

build => 将打包文件分别复制到 Github/blog和Gitee/lanjz 仓库 => 两个 Git 都是执行 commit和push => 然后再分别进入Git Pages 进行更新

使用 GitHub Actions 只需要 push VuePress 项目后,后台就会自己执行上面的所有的操作

# 使用

GitHub 监控到执行事件时,会分配一台虚拟机先将你的项目 checkout 过去,然后按照你指定的 step 顺序执行定义好的 action,这些 action 就包括执行 Node 脚本打包项目,push 到你指定的仓库等动作

粟子:

添加一个 yml 文件并在博客编辑项目的根位置 .github 文件夹中

# .github/pages-update.yml
name: Deploy Github Pages # 该Action的名字

# on:何时触发该事件.
on:
  # 在仓库执行了push请求事件时触发工作流,但只针对主分支
  push:
    branches: [ master ]
  # 允许从Actions选项卡手动运行此工作流
  workflow_dispatch:

# 工作流运行由一个或多个jobs组成,这些job可以按顺序或并行运行
jobs:
  # 此工作流包含一个名为“build-deploy”的job。
  build-deploy:

    runs-on: ubuntu-latest # job运行于什么虚拟机上:最新版 Ubuntu

    # steps表示将作为job一部分执行的一系列任务
    steps:
      - name: Checkout
        uses: actions/checkout@master # 切换分支到master
        with:
          persist-credentials: false

      - name: Build
        uses: actions/setup-node@v1
        with:
          node-version: '12.x'  #使用nodejs 12.x版本

      # 1、生成静态文件
      - name: Build
        run: cd blog&&npm install && npm run build #安装依赖并打包,执行的是项目我们定义的命令
      # 2、更新 Github 博客仓库
      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@releases/v3
        with:
          ACCESS_TOKEN: ${{ secrets.DEPLOY_KEY }} # Github ACCESS_TOKEN
          REPOSITORY_NAME: lanjz/blog # [Github 账号名/仓库]
          BRANCH: master
          FOLDER: blog/docs/.vuepress/dist # 上传到 lanjz/blog 的文件目录,这里就是 vuepress 打包出来的目录

      # 3、同步到 gitee 的仓库
      - name: Sync to Gitee
        uses: wearerequired/git-mirror-action@master
        env:
          # 注意在 Settings->Secrets 配置 GIT id_rsa
          SSH_PRIVATE_KEY: ${{ secrets.GITEE_R_P_KEY }}
        with:
          # 注意替换为你的 GitHub 源仓库地址
          source-repo: git@github.com:lanjz/blog.git
          # 注意替换为你的 Gitee 目标仓库地址
          destination-repo: git@gitee.com:codebeat/lanjz.git # git@gitee.com:[Gitee 账号名]/[仓库].git

      # 4、更新 Gitee Pages
      - name: Build Gitee Pages
        uses: yanglbme/gitee-pages-action@master
        with:
          # 注意替换为你的 Gitee 用户名
          gitee-username: ${{ secrets.GITEE_USERNAME }}
          # 注意在 Settings->Secrets 配置 GITEE_PASSWORD
          gitee-password: ${{ secrets.GITEE_PASSWORD }}
          # 注意替换为你的 Gitee 仓库,仓库名严格区分大小写,请准确填写,否则会出错
          gitee-repo: codebeat/lanjz
          # 要部署的分支,默认是 master,若是其他分支,则需要指定(指定的分支必须存在)
          branch: master

上面的各种 Git 操作难免涉及到权限问题。所以需要一些相关的 key、账号密码等信息

  • ACCESS_TOKEN:这是博客源项目所在 Github 的 ACCESS_TOKEN

    配置位置:Github 头像 -> Settings -> Developer settings => Personal access tokens

  • SSH_PRIVATE_KEY:安装 Github 的时候都会生成私钥和公钥,这里的 SSH_PRIVATE_KEY 就是私钥内容

    配置位置:Github 头像 -> Settings -> Developer settings => Personal access tokens

  • gitee-username:Gitee 账号

  • gitee-password:Gitee 密码

当然这些信息不能直接编写到内容里面。Github 提供了 Actions secrets,类似环境变量的配置,这些环境变量将会被 Action 执行时访问。

配置位置:yml 所在仓库 -> Settings -> secrets

GitHub Actions入门教程:自动化部署静态博客 (opens new window)

github-pages-deploy-action (opens new window)