node腳本實現前端輕量化自動部署

    想直接使用的,github 傳送門 -   git上有詳細配置,記得留個star,筆心

    但最近做的項目修改頻繁,每次部署都是先打包,然後手動拷貝到遠程服務器,次數多了有點麻煩,身爲一個程序員,秉着偷懶的原則,程序能完成的重複工作絕不自己完成,於是就寫了個Node小腳本。

    在寫腳本之前,我們需要了解下package.json,nodejs工程的自動化是依賴於package.json文件中的scripts配置項來實現的,例如使用vue-cli搭建的工程中就會帶有:

{
   ...
   "scripts": {
       "serve": "vue-cli-service serve",
       "build": "vue-cli-service build",
       "lint": "vue-cli-service lint"
     },
   ...
}

     當我們在命令行執行:npm run serve ,其實是在執行vue-cli-service serve,我們也可以自己寫一個自動部署腳本:

當我們在命令行執行:npm run serve ,其實是在執行vue-cli-service serve,我們也可以自己寫一個自動部署腳本:

      我們希望腳本能在npm run build之後自行執行,這裏可以利用npm聲明週期中的post鉤子:

  

  "scripts": {
    "build": "node start.js",
    "dev": "node satrt.js",
    "postbuild": "node deploy.js"
  },

      這裏稍微解釋下npm 聲明週期,我覺得最有用的就是pre和post,如果你想在某個腳本執行完之後執行其他腳本,可以使用前綴+腳本名,這裏使用的是postbuild,同理,prebuild就是在執行build之前執行其他腳本。

      腳本流程如下:

          1.登陸服務器,讀取服務器網站目錄列表,選擇上傳的目錄(你也可以新建目錄)

          2. 備份服務器之前的文件,然後覆蓋上傳。

          3.上傳完畢

  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')
  const ProgressBar =require('progress');
  const  inquirer=require('inquirer')

  //cnpm install moment util events ssh2 progress inquirer --dev  請先在deploy所在目錄安裝以上包

  /******************************請手動配置以下內容*********************************/  
   /** 遠程服務器配置
   * @type {{password: string, port: number, host: string, username: string}}
   */

  const server = {
    host: 'xx.xx.xx.xx',  //主機ip
    port: 22,                //SSH 連接端口,默認22
    username: 'name',        //用戶名
    password: 'pwd',     //用戶登錄密碼
  }
  const basePath = '/web'     //服務器網站根目錄
  let baseDir = ''            //項目目錄名稱
  let  back_up_dir='' //備份目錄名稱,需手動在服務器創建,可選,注意目錄名後有斜槓  比如  back_up/
  const bakDirName = baseDir + '.bak' + moment(new Date()).format('YYYY-M-D-HH:mm:ss')//備份文件名
  const buildPath = path.resolve('./dist')//本地項目編譯後的文件目錄

  /**********************************配置結束***************************************/

  
  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)
  
    // 創建遠程目錄
    console.log('開始創建遠程目錄')
    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
    })
  
    // 上傳文件
    console.log('準備上傳文件:')
    let todoFile = []
    let total=files.length;
    let bar=new ProgressBar('上傳進度:[:bar] :percent   剩餘時長::etas',{total,width: 50});
    files.forEach(function (file) { 
      todoFile.push(function (done) {
        const to = path.join(remoteDir, file.slice(localDir.length + 1)).replace(/[\\]/g, '/')
        // console.log('upload ' + to)
        bar.tick(1);
        doUploadFile(server, file, to, done)
      })
    })
    control.emit('doNext', todoDir, function (err) {
      if (err) {
        throw err
      }
      else {
        control.emit('doNext', todoFile, then)
      }
    })
  }

  
  let mutual={
    chooseDir:function (err,dirList){
        if(err){
            console.log(err)
            return false
        }
        dirList.unshift('我要新建目錄')
          const promptList = [
              {
                  type: 'list',
                  message: '請選擇要上傳到的項目目錄:',
                  name: 'dir',
                  choices:dirList,
              }
          ];
      
          inquirer.prompt(promptList).then(answers => {
              
              if(answers.dir==='我要新建目錄'){
                  mutual.mkNewDir()
                  return false
              }else if(answers.dir.includes(basePath)){
                baseDir=basePath
              }
              else{
                baseDir=answers.dir
              }
              init()
        
          }).catch(err=>{
              console.log(err)
          })
    },
    mkNewDir:function(){
        const promptList = [
            {
                type: 'input',
                message: '請輸入要創建的目錄名稱:',
                name: 'dir',    
        
            }
        ];
    
        inquirer.prompt(promptList).then(answers => {
            if(!answers.dir){
                console.error('警告:文件名不能爲空')
                return false
            }
            baseDir=answers.dir
            init()
        })
    }

  }




/**
* 描述:獲取遠程文件路徑下文件列表信息
* 參數:server 遠程電腦憑證;
*		remotePath 遠程路徑;
*		isFile 是否是獲取文件,true獲取文件信息,false獲取目錄信息;
*		then 回調函數
* 回調:then(err, dirs) : dir, 獲取的列表信息
*/
function getFileOrDirList(server, remotePath, isFile, then){
	var cmd = "find " + remotePath + " -type "+ (isFile == true ? "f":"d") + "\r\nexit\r\n";	
	doShell(server, cmd, function(err, data){
		var arr = [];
		var remoteFile = [];
		arr = data.split("\r\n");
		arr.forEach(function(dir){
			if(dir.indexOf(remotePath) ==0){
				remoteFile.push(dir);
			}
        });
        remoteFile=remoteFile.map(item=>item.split('/')[2]).filter(item=> item&&item.trim()) //只保留第一層目錄
        remoteFile.push(basePath+'(項目根目錄)') //上傳網站首頁文件地址
        remoteFile=Array.from(new Set(remoteFile)) //去重
		then(err, remoteFile);
    })
};

  
function init(){
    console.log('\n--------配置如下--------------\n')
    console.log(`服務器host:            ${server.host}`)
    console.log(`項目文件夾:            ${baseDir}`)
    console.log(`項目部署以及備份目錄:  ${basePath}`)
    console.log(`備份後的文件夾名:      ${bakDirName}`)
    console.log('\n--------開始部署--------------\n')
    doShell(server, `mv ${basePath}/${baseDir} ${basePath}/${ back_up_dir}${bakDirName}\nexit\n`) //備份遠程目錄文件
    doUploadDir(server, buildPath, `${basePath}/${baseDir}`, () => console.log('\n--------部署成功--------------'))
}


getFileOrDirList(server,basePath,false,mutual.chooseDir)

     代碼參考:https://github.com/hello-jun/deploy

     因爲我本地一個文件目錄下有多個項目,上傳到的文件服務器目錄也不同,因爲我利用inquirer 添加了用戶交互功能,你可以選擇任意一個目錄或新建目錄上傳,此外還新增了上傳進度條,可以對上傳進度一目瞭然。

     建議:爲了避免賬號密碼泄露,賬號密碼最好從其他文件夾中導入,免得多人共享git導致密碼泄露,登陸賬號權限最好也不是超級管理員。

    使用截圖:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章