基於node.js實現前端項目自動化部署(懶人福音)

爲何寫這個?

真相只有一個=>懶!!!css

想直接使用的, github 傳送門 後面的能夠不用看了- -,記得留個star,筆心

目前主要在作iot項目,因爲歷史緣由,平臺還存在許多react/vue的純H5子項目,這些項目又必須調用APP暴露的某些api,使得本地開發調試時不得不重複構建手動將build包部署到開發服務器上。幾個項目,幾輪轉測試下來,很有些心累。所以想着能不能寫個工具,在執行yarn build構建打包後自動部署到服務器,這樣就能夠省去:打開ftp工具->尋找構建目錄->尋找服務器上部署目錄->備份->粘貼複製文件,這套繁瑣手動流程,提升工(mo)做(yu)效率。
而且,有了這個後,jenkins自動構建時,項目配置的shell腳本也能少幾行代碼。html

怎麼實現?

思路分析

首先,咱們先分析下要達到的結果:前端

  1. 執行yarn buildvue

    我的喜歡用 yarn,用 npm的同窗能夠本身替換。
    假設項目都是基於 create react app || Vue cli 腳手架搭建的,若是是本身自定義的腳手架的話,請繼續往下看。本文以 create react app腳手架爲例
  2. 工具自動觸發
  3. 登錄遠程服務器,創建鏈接,備份原有項目
  4. 從本地構建目錄(本文爲項目根目錄下的build目錄)copy全部文件並上傳至遠程服務器部署目錄
  5. 部署完成,輸出提示,關閉遠程鏈接

其次,分析如何達成結果:node

  1. 前端項目中寫工具,首選node.js
  2. 工具須要在執行yarn build之後自動觸發,這裏咱們須要用到npm scripts中的鉤子postbuild ,它會在執行build後作一些收尾工做,正好能夠用來觸發部署操做。對npm scripts不太瞭解的同窗,能夠參閱《阮一峯 npm scripts 使用指南》
  3. 與服務器創建鏈接,並進行操做,須要創建ssh鏈接,推薦使用第三方庫ssh2
  4. 備份時,文件名最好加上具體時間。例如:my_app.bak2019-10-8-14:36:36。爲了方便,直接使用一個輕量級時間庫moment

開幹

  1. 安裝 ssh2 、momentreact

    yarn add ssh2 moment
  2. 爲了方便起見,在項目根目錄下新建 deploy.js 。這個js文件就是本次編寫的自動化部署工具git

    deploy.js不必定要在項目根目錄下,若是你對node尋找路徑方式比較熟悉,能夠放在本身指定的路徑下面
  3. 打開 package.json文件,在 scripts中添加:github

    "postbuild": "yarn run deploy",  
    "deploy": "node ./deploy.js",

    此時,若是往 deploy.js 中添加 console.log('---deploy test--'),控制檯執行 yarn build,能夠看到,在構建完成後,會繼續執行yarn run deploy,最終控制檯輸出:shell

    ---deploy test--
  4. 根據ssh2文檔,編寫鏈接服務器、備份文件、上傳文件的代碼:
const path = require('path')
const moment = require('moment')
const util = require('util')
const events = require('events')
const Client = require('ssh2').Client
const fs = require('fs')
/*********************************************************************************/
/******************************請手動配置如下內容*********************************/
/*********************************************************************************/
/**
 * 遠程服務器配置
 * @type {{password: string, port: number, host: string, username: string}}
 */
const server = {
  host: 'xxx.xxx.xxx.xxx',  //主機ip
  port: 22,                //SSH 鏈接端口
  username: 'xxxx',        //用戶名
  password: 'xxxxxxx',     //用戶登陸密碼
}
const baseDir = 'my_app'//項目目錄
const basePath = '/home'//項目部署目錄
const bakDirName = baseDir + '.bak' + moment(new Date()).format('YYYY-M-D-HH:mm:ss')//備份文件名
const buildPath = path.resolve('./build')//本地項目編譯後的文件目錄
/*********************************************************************************/
/**********************************配置結束***************************************/
/*********************************************************************************/

function doConnect(server, then) {
  const conn = new Client()
  conn.on('ready', function () {
    then && then(conn)
  }).on('error', function (err) {
    console.error('connect error!', err)
  }).on('close', function () {
    conn.end()
  }).connect(server)
}

function doShell(server, cmd, then) {
  doConnect(server, function (conn) {
    conn.shell(function (err, stream) {
      if (err) throw err
      else {
        let buf = ''
        stream.on('close', function () {
          conn.end()
          then && then(err, buf)
        }).on('data', function (data) {
          buf = buf + data
        }).stderr.on('data', function (data) {
          console.log('stderr: ' + data)
        })
        stream.end(cmd)
      }
    })
  })
}

function doGetFileAndDirList(localDir, dirs, files) {
  const dir = fs.readdirSync(localDir)
  for (let i = 0; i < dir.length; i++) {
    const p = path.join(localDir, dir[i])
    const stat = fs.statSync(p)
    if (stat.isDirectory()) {
      dirs.push(p)
      doGetFileAndDirList(p, dirs, files)
    }
    else {
      files.push(p)
    }
  }
}

function Control() {
  events.EventEmitter.call(this)
}

util.inherits(Control, events.EventEmitter)

const control = new Control()

control.on('doNext', function (todos, then) {
  if (todos.length > 0) {
    const func = todos.shift()
    func(function (err, result) {
      if (err) {
        then(err)
        throw err
      }
      else {
        control.emit('doNext', todos, then)
      }
    })
  }
  else {
    then(null)
  }
})

function doUploadFile(server, localPath, remotePath, then) {
  doConnect(server, function (conn) {
    conn.sftp(function (err, sftp) {
      if (err) {
        then(err)
      }
      else {
        sftp.fastPut(localPath, remotePath, function (err, result) {
          conn.end()
          then(err, result)
        })
      }
    })
  })
}

function doUploadDir(server, localDir, remoteDir, then) {
  let dirs = []
  let files = []
  doGetFileAndDirList(localDir, dirs, files)

  // 建立遠程目錄
  let todoDir = []
  dirs.forEach(function (dir) {
    todoDir.push(function (done) {
      const to = path.join(remoteDir, dir.slice(localDir.length + 1)).replace(/[\\]/g, '/')
      const cmd = 'mkdir -p ' + to + '\r\nexit\r\n'
      console.log(`cmd::${cmd}`)
      doShell(server, cmd, done)
    })// end of push
  })

  // 上傳文件
  let todoFile = []
  files.forEach(function (file) {
    todoFile.push(function (done) {
      const to = path.join(remoteDir, file.slice(localDir.length + 1)).replace(/[\\]/g, '/')
      console.log('upload ' + to)
      doUploadFile(server, file, to, done)
    })
  })
  control.emit('doNext', todoDir, function (err) {
    if (err) {
      throw err
    }
    else {
      control.emit('doNext', todoFile, then)
    }
  })
}

console.log('--------deploy config--------------')
console.log(`服務器host:            ${server.host}`)
console.log(`項目文件夾:            ${baseDir}`)
console.log(`項目部署以及備份目錄:  ${basePath}`)
console.log(`備份後的文件夾名:      ${bakDirName}`)
console.log('--------deploy start--------------')

doShell(server, `mv ${basePath}/${baseDir} ${basePath}/${bakDirName}\nexit\n`)

doUploadDir(server, buildPath, `${basePath}/${baseDir}`, () => console.log('--------deploy end--------------'))

運行結果示例

使用scripts觸發時,運行yarn build之後,會自動觸發生命週期鉤子 postbuild,進行部署。此過程會先在本地構建打包項目至配置的buildPath目錄,而後在遠程服務器xxx.xxx.xxx.xxx /home中將my_app備份爲my_app.bak2019-10-8-23:06:27,最後將本地buildPath目錄文件所有上傳到/home/my_app,完成部署。npm

$ yarn run deploy
$ node ./deploy.js
--------deploy config--------------
服務器host:           xxx.xxx.xxx.xxx
項目文件夾:            my_app
項目部署以及備份目錄:    /home
備份後的文件夾名:       my_app.bak2019-10-8-23:06:27
--------deploy start--------------
cmd::mkdir -p /home/my_app/static
exit

cmd::mkdir -p /home/my_app/static/css
exit

cmd::mkdir -p /home/my_app/static/js
exit

cmd::mkdir -p /home/my_app/static/media
exit

upload /home/my_app/asset-manifest.json
upload /home/my_app/favicon.ico
upload /home/my_app/index.html
upload /home/my_app/logo192.png
upload /home/my_app/logo512.png
upload /home/my_app/manifest.json
upload /home/my_app/precache-manifest.20dc8cb74286fd491ca0a9fc9b07234a.js
upload /home/my_app/robots.txt
upload /home/my_app/service-worker.js
upload /home/my_app/static/css/main.2cce8147.chunk.css
upload /home/my_app/static/css/main.2cce8147.chunk.css.map
upload /home/my_app/static/js/2.222d1515.chunk.js
upload /home/my_app/static/js/2.222d1515.chunk.js.map
upload /home/my_app/static/js/main.0782b2ff.chunk.js
upload /home/my_app/static/js/main.0782b2ff.chunk.js.map
upload /home/my_app/static/js/runtime~main.077bb605.js
upload /home/my_app/static/js/runtime~main.077bb605.js.map
upload /home/my_app/static/media/logo.5d5d9eef.svg
--------deploy end--------------

Done in 16.58s.

結語

項目GitHub地址: https://github.com/hello-jun/deploy此工具也能夠單獨使用,稍加改造後,也能夠用來自動部署react native項目,有興趣的能夠本身嘗試~歡迎star、留言、issue。但願本文對各位有所幫助,祝工做生活愉快!

相關文章
相關標籤/搜索