前端工程化中的自動化部署

前言

       在前端工程化中,前端開發人員終於在不斷的提升本身的地位,不再是簡單的切圖仔了。固然,隨之而來的就是咱們的工做內容變得愈來愈多,變得愈來愈繁瑣。不只要不斷的學習新的前端技術內容,並且還要獨立維護前端的部署工做。所以,如何能快速的進行工程化內容的部署,就是一件很是有價值的事情。

快速部署

       對於前端的部署來講,其實主要就是將編譯後的工程化項目(以vue來講就是將npm run build的dist文件夾)直接部署到對應的服務器的指定目錄下便可,其餘的內容咱們暫時不在此處作過多的講解。html

       所以,目前通常會使用SFTP工具(如:FileZilla:https://www.filezilla.cn/,Cyberduck:https://cyberduck.io/)來上傳構建好的前端代碼到服務器。這種方式雖然也是比較快的,可是每次要本身進行打包編譯=》壓縮=》打開對應的工具=》找到對應的路徑,手動的將相關的文件上傳到對應的服務中。而且爲了部署到不一樣的環境中(通常項目開發包括:dev開發環境、test測試環境、uat測試環境、pro測試環境等)還要重複的進行屢次的重複性操做。這種方式不只繁瑣,並且還下降了咱們程序員的身份。因此咱們須要用自動化部署來代替手動 ,這樣一方面能夠提升部署效率,另外一方面開發人員能夠少作一些他們覺得的無用功。前端

準備工做

       話很少說,說幹就幹。首先,咱們先梳理一下通常的部署流程,主要是先將本地的工程化代碼進行打包編譯=>經過ssh登陸到服務器=>將本地編譯的代碼上傳到服務器指定的路徑中,具體流程以下:

       所以,經過代碼實現自動化部署腳本,主要須要實現以下,下面以vue項目來爲工程項目來具體講解:vue

  1. 實現本地代碼編譯;可直接配置npm run build便可進行相關的打包
  2. 實現本地編譯的代碼壓縮

3.經過ssh鏈接遠程服務器node

  1. 檢查對應的遠程部署路徑是否存在,不存在須要建立
  2. 上傳本地的壓縮包
  3. 解壓上傳的壓縮包
  4. 刪除上傳的壓縮包
  5. 關閉ssh鏈接
  6. 刪除本地的壓縮包

具體實現

爲了在可以實現以上的幾個步驟,咱們須要引入一些依賴包,以此來進行相關步驟的操做和日誌的打印輸出

大體流程以下
git

基本配置

       爲了可以將部署相關的內容,如服務器信息,部署的路徑等內容進行統一的管理,以便進行將來的可視化配置。所以,在項目中建立一個獨立的文件夾,統一管理部署相關的文件,而且創建一個config.js和ssh.js文件,分別用於配置相關的部署信息和具體的部署腳本。其中,config.js中的配置具體以下:程序員

const config = {
  // 開發環境
  dev: {
    host: '',
    username: 'root',
    password: '',
    catalog: '', // 前端文件壓縮目錄
    port: 22, // 服務器ssh鏈接端口號
    privateKey: null // 私鑰,私鑰與密碼二選一
  },
  // 測試環境
  test: {
    host: '', // 服務器ip地址或域名
    username: 'root', // ssh登陸用戶
    password: '', // 密碼
    catalog: '', // 前端文件壓縮目錄
    port: 22, // 服務器ssh鏈接端口號
    privateKey: null // 私鑰,私鑰與密碼二選一
  },
  // 線上環境
  pro: {
    host: '', // 服務器ip地址或域名
    username: 'root', // ssh登陸用戶
    password: '', // 密碼,請勿將此密碼上傳至git服務器
    catalog: '', // 前端文件壓縮目錄
    port: 22, // 服務器ssh鏈接端口號
    privateKey: null // 私鑰,私鑰與密碼二選一
  }
}

module.exports = {
  // publishEnv: pro,
  publishEnv: config.pro, // 發佈環境
  buildDist: 'dist', // 前端文件打包以後的目錄,默認dist
  buildCommand: 'npm run build', // 打包前端文件的命令
  readyTimeout: 20000, // ssh鏈接超時時間
  deleteFile: true, // 是否刪除線上上傳的dist壓縮包
  isNeedBuild: true // s是否須要打包
}

壓縮打包內容

       壓縮打包的內容,使用JSZIP插件,初始化一個const zip = new JSZIP(),而後在按照對應的語法進行具體的實現,其實現過程主要是經過的先遞歸讀取相關的文件,而後加入到zip對象中,最後經過插件進行具體的壓縮,最後寫入到磁盤中。具體代碼以下:
遞歸讀取打包文件github

// 讀取文件
  readDir (obj, nowPath) {
    const files = fs.readdirSync(nowPath) // 讀取目錄中的全部文件及文件夾(同步操做)
    files.forEach((fileName, index) => {
      // 遍歷檢測目錄中的文件
      // console.log(fileName, index) // 打印當前讀取的文件名
      const fillPath = nowPath + '/' + fileName
      const file = fs.statSync(fillPath) // 獲取一個文件的屬性
      if (file.isDirectory()) {
        // 若是是目錄的話,繼續查詢
        const dirlist = zip.folder(fileName) // 壓縮對象中生成該目錄
        this.readDir(dirlist, fillPath) // 從新檢索目錄文件
      } else {
        obj.file(fileName, fs.readFileSync(fillPath)) // 壓縮目錄添加文件
      }
    })
  }

壓縮文件夾下的全部文件算法

// 壓縮文件夾下的全部文件
  zipFile (filePath) {
    return new Promise((resolve, reject) => {
      let desc =
         '*******************************************\n' +
         '***                正在壓縮              ***\n' +
         '*******************************************\n'
      console.log(chalk.blue(desc))
      this.readDir(zip, filePath)
      zip
        .generateAsync({
          // 設置壓縮格式,開始打包
          type: 'nodebuffer', // nodejs用
          compression: 'DEFLATE', // 壓縮算法
          compressionOptions: {
            // 壓縮級別
            level: 9
          }
        })
        .then(content => {
          fs.writeFileSync(
            path.join(rootDir, '/', this.fileName),
            content,
            'utf-8'
          )
          desc =
            '*******************************************\n' +
            '***                壓縮成功              ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
          resolve({
            success: true
          })
        }).catch(err => {
          console.log(chalk.red(err))
          reject(err)
        })
    })
  }

使用child_process的exec來運行npm run build腳本打包shell

// 打包本地前端文件
  buildProject () {
    return new Promise((resolve, reject) => {
      exec(Config.buildCommand, async (error, stdout, stderr) => {
        if (error) {
          console.error(error)
          reject(error)
        } else if (stdout) {
          resolve({
            stdout,
            success: true
          })
        } else {
          console.error(stderr)
          reject(stderr)
        }
      })
    })
  }

鏈接ssh服務器

經過ssh2的client來建立ssh鏈接npm

// 鏈接服務器
  connectServer () {
    return new Promise((resolve, reject) => {
      const conn = this.conn
      conn
        .on('ready', () => {
          resolve({
            success: true
          })
        })
        .on('error', (err) => {
          reject(err)
        })
        .on('end', () => {
          const desc =
            '*******************************************\n' +
            '***           SSH鏈接已結束        ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
        })
        .on('close', () => {
          const desc =
            '*******************************************\n' +
            '***           SSH鏈接已關閉        ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
        })
        .connect(this.server)
    })
  }

上傳壓縮的工程文件

判斷上傳路徑是否存在

// 判斷文件是否存在,若是不存在則進行建立文件夾
  await sshCon.execSsh(
    `
    if [[ ! -d ${sshConfig.catalog} ]]; then
     mkdir -p ${sshConfig.catalog}
    fi
    `
  )

經過client的sftp講本地的壓縮包上傳到指定的服務器的對應地址

// 上傳文件
  uploadFile ({ localPath, remotePath }) {
    return new Promise((resolve, reject) => {
      return this.conn.sftp((err, sftp) => {
        if (err) {
          reject(err)
        } else {
          sftp.fastPut(localPath, remotePath, (err, result) => {
            if (err) {
              reject(err)
            }
            resolve({
              success: true,
              result
            })
          })
        }
      })
    })
  }

解壓上傳的工程文件

經過exec來執行ssh命令的

// 執行ssh命令
  execSsh (command) {
    return new Promise((resolve, reject) => {
      return this.conn.exec(command, (err, stream) => {
        if (err || !stream) {
          reject(err)
        } else {
          stream
            .on('close', (code, signal) => {
              resolve({
                success: true
              })
            })
            .on('data', function (data) {

            })
            .stderr.on('data', function (data) {
              resolve({
                success: false,
                error: data.toString()
              })
            })
        }
      })
    })
  }

解壓上傳的壓縮文件

desc =
   '*******************************************\n' +
   '***       上傳文件成功,開始解壓文件      ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))
  const 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}----`)
    return false
  } else if (Config.deleteFile) {
    desc =
       '*******************************************\n' +
       '***   解壓文件成功,開始刪除上傳的壓縮包   ***\n' +
       '*******************************************\n'
    console.log(chalk.green(desc))

刪除上傳的工程文件

刪除對應的壓縮包

desc =
       '*******************************************\n' +
       '***   解壓文件成功,開始刪除上傳的壓縮包   ***\n' +
       '*******************************************\n'
    console.log(chalk.green(desc))
    // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改
    const deleteZipRes = await sshCon
      .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`)
      .catch((e) => {})
    if (!deleteZipRes || !deleteZipRes.success) {
      console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----'))
      console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`))
      return false
    }

關閉ssh鏈接

封裝關閉的服務器鏈接

// 結束鏈接
  endConn () {
    this.conn.end()
    if (this.connAgent) {
      this.connAgent.end()
    }
  }

刪除本地的壓縮包文件

封裝刪除本地的壓縮包

// 刪除本地文件
  deleteLocalFile () {
    return new Promise((resolve, reject) => {
      fs.unlink(path.join(rootDir, '/', this.fileName), function (error) {
        if (error) {
          const desc =
            '*******************************************\n' +
            '***            本地文件刪除失敗          ***\n' +
            '*******************************************\n'
          console.log(chalk.yellow(desc))
          reject(error)
        } else {
          const desc =
            '*******************************************\n' +
            '***              刪除成功               ***\n' +
            '*******************************************\n'
          console.log(chalk.blue(desc))
          resolve({
            success: true
          })
        }
      })
    })
  }

腳本命令的配置

在項目的package.json中配置命令

"ssh": "node ./build/ssh.js"

完整的實現ssh流程

// SSH鏈接,上傳,解壓,刪除等相關操做
async function sshUpload (sshConfig, fileName) {
  const sshCon = new SSH(sshConfig)
  const sshRes = await sshCon.connectServer().catch((e) => {
    console.error(e)
  })
  if (!sshRes || !sshRes.success) {
    const desc =
       '*******************************************\n' +
       '*** ssh鏈接失敗 ***\n' +
       '*******************************************\n'
    console.log(chalk.red(desc))
    return false
  }
  let desc =
   '*******************************************\n' +
   '***      鏈接服務器成功,開始上傳文件     ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))
  // 判斷文件是否存在,若是不存在則進行建立文件夾
  await sshCon.execSsh(
    `
    if [[ ! -d ${sshConfig.catalog} ]]; then
     mkdir -p ${sshConfig.catalog}
    fi
    `
  )
  const uploadRes = await sshCon
    .uploadFile({
      localPath: path.join(rootDir, '/', fileName),
      remotePath: sshConfig.catalog + '/' + fileName
    })
    .catch((e) => {
      console.error(e)
    })

  if (!uploadRes || !uploadRes.success) {
    console.error('----上傳文件失敗,請從新上傳----')
    return false
  }
  desc =
   '*******************************************\n' +
   '***       上傳文件成功,開始解壓文件      ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))
  const 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}----`)
    return false
  } else if (Config.deleteFile) {
    desc =
       '*******************************************\n' +
       '***   解壓文件成功,開始刪除上傳的壓縮包   ***\n' +
       '*******************************************\n'
    console.log(chalk.green(desc))
    // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改
    const deleteZipRes = await sshCon
      .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`)
      .catch((e) => {})
    if (!deleteZipRes || !deleteZipRes.success) {
      console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----'))
      console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`))
      return false
    }
  }
  // 結束ssh鏈接
  sshCon.endConn()
  return true
}

實際運行腳本

// 執行前端部署
;(async () => {
  const file = new File()
  let desc =
    '*******************************************\n' +
    '***              開始編譯               ***\n' +
    '*******************************************\n'
  if (Config.isNeedBuild) {
    console.log(chalk.green(desc))
    // 打包文件
    const buildRes = await file
      .buildProject()
      .catch((e) => {
        console.error(e)
      })
    if (!buildRes || !buildRes.success) {
      desc =
            '*******************************************\n' +
            '***          打包出錯,請檢查錯誤         ***\n' +
            '*******************************************\n'
      console.log(chalk.red(desc))
      return false
    }
    console.log(chalk.blue(buildRes.stdout))
    desc =
          '*******************************************\n' +
          '***              編譯成功               ***\n' +
          '*******************************************\n'
    console.log(chalk.green(desc))
  }
  // 壓縮文件
  const res = await file
    .zipFile(path.join(rootDir, '/', Config.buildDist))
    .catch(() => {})
  if (!res || !res.success) return false
  desc =
   '*******************************************\n' +
   '***              開始部署               ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))

  const bol = await sshUpload(Config.publishEnv, file.fileName)
  if (bol) {
    desc =
     '\n******************************************\n' +
     '***              部署成功              ***\n' +
     '******************************************\n'
    console.log(chalk.green(desc))
    file.stopProgress()
  } else {
    process.exit(1)
  }
})()

完整的ssh.js代碼

const { exec } = require('child_process')
const path = require('path')
const JSZIP = require('jszip')
const fs = require('fs')
const Client = require('ssh2').Client
const Config = require('./config.js')
const chalk = require('chalk')
const zip = new JSZIP()
// 前端打包文件的目錄
const rootDir = path.resolve(__dirname, '..')

/**
 * ssh鏈接
 */
class SSH {
  constructor ({ host, port, username, password, privateKey }) {
    this.server = {
      host,
      port,
      username,
      password,
      privateKey
    }
    this.conn = new Client()
  }

  // 鏈接服務器
  connectServer () {
    return new Promise((resolve, reject) => {
      const conn = this.conn
      conn
        .on('ready', () => {
          resolve({
            success: true
          })
        })
        .on('error', (err) => {
          reject(err)
        })
        .on('end', () => {
          const desc =
            '*******************************************\n' +
            '***           SSH鏈接已結束        ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
        })
        .on('close', () => {
          const desc =
            '*******************************************\n' +
            '***           SSH鏈接已關閉        ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
        })
        .connect(this.server)
    })
  }

  // 上傳文件
  uploadFile ({ localPath, remotePath }) {
    return new Promise((resolve, reject) => {
      return this.conn.sftp((err, sftp) => {
        if (err) {
          reject(err)
        } else {
          sftp.fastPut(localPath, remotePath, (err, result) => {
            if (err) {
              reject(err)
            }
            resolve({
              success: true,
              result
            })
          })
        }
      })
    })
  }

  // 執行ssh命令
  execSsh (command) {
    return new Promise((resolve, reject) => {
      return this.conn.exec(command, (err, stream) => {
        if (err || !stream) {
          reject(err)
        } else {
          stream
            .on('close', (code, signal) => {
              resolve({
                success: true
              })
            })
            .on('data', function (data) {

            })
            .stderr.on('data', function (data) {
              resolve({
                success: false,
                error: data.toString()
              })
            })
        }
      })
    })
  }

  // 結束鏈接
  endConn () {
    this.conn.end()
    if (this.connAgent) {
      this.connAgent.end()
    }
  }
}

/*
 * 本地操做
 * */
class File {
  constructor () {
    this.fileName = this.formateName()
  }

  // 刪除本地文件
  deleteLocalFile () {
    return new Promise((resolve, reject) => {
      fs.unlink(path.join(rootDir, '/', this.fileName), function (error) {
        if (error) {
          const desc =
            '*******************************************\n' +
            '***            本地文件刪除失敗          ***\n' +
            '*******************************************\n'
          console.log(chalk.yellow(desc))
          reject(error)
        } else {
          const desc =
            '*******************************************\n' +
            '***              刪除成功               ***\n' +
            '*******************************************\n'
          console.log(chalk.blue(desc))
          resolve({
            success: true
          })
        }
      })
    })
  }

  // 讀取文件
  readDir (obj, nowPath) {
    const files = fs.readdirSync(nowPath) // 讀取目錄中的全部文件及文件夾(同步操做)
    files.forEach((fileName, index) => {
      // 遍歷檢測目錄中的文件
      // console.log(fileName, index) // 打印當前讀取的文件名
      const fillPath = nowPath + '/' + fileName
      const file = fs.statSync(fillPath) // 獲取一個文件的屬性
      if (file.isDirectory()) {
        // 若是是目錄的話,繼續查詢
        const dirlist = zip.folder(fileName) // 壓縮對象中生成該目錄
        this.readDir(dirlist, fillPath) // 從新檢索目錄文件
      } else {
        obj.file(fileName, fs.readFileSync(fillPath)) // 壓縮目錄添加文件
      }
    })
  }

  // 壓縮文件夾下的全部文件
  zipFile (filePath) {
    return new Promise((resolve, reject) => {
      let desc =
         '*******************************************\n' +
         '***                正在壓縮              ***\n' +
         '*******************************************\n'
      console.log(chalk.blue(desc))
      this.readDir(zip, filePath)
      zip
        .generateAsync({
          // 設置壓縮格式,開始打包
          type: 'nodebuffer', // nodejs用
          compression: 'DEFLATE', // 壓縮算法
          compressionOptions: {
            // 壓縮級別
            level: 9
          }
        })
        .then(content => {
          fs.writeFileSync(
            path.join(rootDir, '/', this.fileName),
            content,
            'utf-8'
          )
          desc =
            '*******************************************\n' +
            '***                壓縮成功              ***\n' +
            '*******************************************\n'
          console.log(chalk.green(desc))
          resolve({
            success: true
          })
        }).catch(err => {
          console.log(chalk.red(err))
          reject(err)
        })
    })
  }

  // 打包本地前端文件
  buildProject () {
    return new Promise((resolve, reject) => {
      exec(Config.buildCommand, async (error, stdout, stderr) => {
        if (error) {
          console.error(error)
          reject(error)
        } else if (stdout) {
          resolve({
            stdout,
            success: true
          })
        } else {
          console.error(stderr)
          reject(stderr)
        }
      })
    })
  }

  // 中止程序以前需刪除本地壓縮包文件
  stopProgress () {
    this.deleteLocalFile()
      .catch((e) => {
        console.log(chalk.red('----刪除本地文件失敗,請手動刪除----'))
        console.log(chalk.red(e))
        process.exit(1)
      })
      .then(() => {
        const desc =
           '*******************************************\n' +
           '***          已刪除本地壓縮包文件        ***\n' +
           '*******************************************\n'
        console.log(chalk.green(desc))
        process.exitCode = 0
      })
  }

  // 格式化命名文件名稱
  formateName () {
    // 壓縮包的名字
    const date = new Date()
    const year = date.getFullYear()
    const month = date.getMonth() + 1
    const day = date.getDate()
    const timeStr = `${year}_${month}_${day}`
    return `${Config.buildDist}-${timeStr}-${Math.random()
      .toString(16)
      .slice(2)}.zip`
  }
}

// SSH鏈接,上傳,解壓,刪除等相關操做
async function sshUpload (sshConfig, fileName) {
  const sshCon = new SSH(sshConfig)
  const sshRes = await sshCon.connectServer().catch((e) => {
    console.error(e)
  })
  if (!sshRes || !sshRes.success) {
    const desc =
       '*******************************************\n' +
       '*** ssh鏈接失敗 ***\n' +
       '*******************************************\n'
    console.log(chalk.red(desc))
    return false
  }
  let desc =
   '*******************************************\n' +
   '***      鏈接服務器成功,開始上傳文件     ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))
  // 判斷文件是否存在,若是不存在則進行建立文件夾
  await sshCon.execSsh(
    `
    if [[ ! -d ${sshConfig.catalog} ]]; then
     mkdir -p ${sshConfig.catalog}
    fi
    `
  )
  const uploadRes = await sshCon
    .uploadFile({
      localPath: path.join(rootDir, '/', fileName),
      remotePath: sshConfig.catalog + '/' + fileName
    })
    .catch((e) => {
      console.error(e)
    })

  if (!uploadRes || !uploadRes.success) {
    console.error('----上傳文件失敗,請從新上傳----')
    return false
  }
  desc =
   '*******************************************\n' +
   '***       上傳文件成功,開始解壓文件      ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))
  const 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}----`)
    return false
  } else if (Config.deleteFile) {
    desc =
       '*******************************************\n' +
       '***   解壓文件成功,開始刪除上傳的壓縮包   ***\n' +
       '*******************************************\n'
    console.log(chalk.green(desc))
    // 注意:rm -rf爲危險操做,請勿對此段代碼作其餘非必須更改
    const deleteZipRes = await sshCon
      .execSsh(`rm -rf ${sshConfig.catalog + '/' + fileName}`)
      .catch((e) => {})
    if (!deleteZipRes || !deleteZipRes.success) {
      console.log(chalk.pink('----刪除文件失敗,請手動刪除zip文件----'))
      console.log(chalk.red(`----錯誤緣由:${deleteZipRes.error}----`))
      return false
    }
  }
  // 結束ssh鏈接
  sshCon.endConn()
  return true
}
// 執行前端部署
;(async () => {
  const file = new File()
  let desc =
    '*******************************************\n' +
    '***              開始編譯               ***\n' +
    '*******************************************\n'
  if (Config.isNeedBuild) {
    console.log(chalk.green(desc))
    // 打包文件
    const buildRes = await file
      .buildProject()
      .catch((e) => {
        console.error(e)
      })
    if (!buildRes || !buildRes.success) {
      desc =
            '*******************************************\n' +
            '***          打包出錯,請檢查錯誤         ***\n' +
            '*******************************************\n'
      console.log(chalk.red(desc))
      return false
    }
    console.log(chalk.blue(buildRes.stdout))
    desc =
          '*******************************************\n' +
          '***              編譯成功               ***\n' +
          '*******************************************\n'
    console.log(chalk.green(desc))
  }
  // 壓縮文件
  const res = await file
    .zipFile(path.join(rootDir, '/', Config.buildDist))
    .catch(() => {})
  if (!res || !res.success) return false
  desc =
   '*******************************************\n' +
   '***              開始部署               ***\n' +
   '*******************************************\n'
  console.log(chalk.green(desc))

  const bol = await sshUpload(Config.publishEnv, file.fileName)
  if (bol) {
    desc =
     '\n******************************************\n' +
     '***              部署成功              ***\n' +
     '******************************************\n'
    console.log(chalk.green(desc))
    file.stopProgress()
  } else {
    process.exit(1)
  }
})()
相關文章
相關標籤/搜索