4

为什么写这个?

真相只有一个=>懒!!!

想直接使用的,github 传送门 后面的可以不用看了- -,记得留个star,笔心

目前主要在做iot项目,由于历史原因,平台还存在许多react/vue的纯H5子项目,这些项目又必须调用APP暴露的某些api,使得本地开发调试时不得不重复构建手动将build包部署到开发服务器上。几个项目,几轮转测试下来,颇有些心累。因此想着能不能写个工具,在执行yarn build构建打包后自动部署到服务器,这样就可以省去:打开ftp工具->寻找构建目录->寻找服务器上部署目录->备份->粘贴复制文件,这套繁琐手动流程,提高工(mo)作(yu)效率。
并且,有了这个后,jenkins自动构建时,项目配置的shell脚本也能少几行代码。

怎么实现?

思路分析

首先,我们先分析下要达到的结果:

  1. 执行yarn build

    个人喜欢用 yarn,用npm的同学可以自己替换。
    假设项目都是基于create react app || Vue cli 脚手架搭建的,如果是自己自定义的脚手架的话,请继续往下看。本文以 create react app脚手架为例
  2. 工具自动触发
  3. 登陆远程服务器,建立连接,备份原有项目
  4. 从本地构建目录(本文为项目根目录下的build目录)copy所有文件并上传至远程服务器部署目录
  5. 部署完成,输出提示,关闭远程连接

其次,分析如何达成结果:

  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 、moment

    yarn add ssh2 moment
  2. 为了方便起见,在项目根目录下新建 deploy.js 。这个js文件就是本次编写的自动化部署工具

    deploy.js不一定要在项目根目录下,如果你对node寻找路径方式比较熟悉,可以放在自己指定的路径下面
  3. 打开 package.json文件,在 scripts中添加:

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

    此时,如果往 deploy.js 中添加 console.log('---deploy test--'),控制台执行 yarn build,可以看到,在构建完成后,会继续执行yarn run deploy,最终控制台输出:

    ---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,完成部署。

$ 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。
希望本文对各位有所帮助,祝工作生活愉快!


张子枫
789 声望81 粉丝

主食React/React Native , 零食Vue