Automated deployment of front-end projects based on node.js (gospel of lazy people)

Keywords: Javascript npm React github JSON

Why did you write this?

There is only one truth => laziness!!!

If you want to use it directly, github portal You don't have to look at the back - - remember to leave a star, the heart of your pen.

At present, iot project is mainly done. Due to historical reasons, there are many pure H5 sub-projects of react/vue on the platform. These projects must call some APIs exposed by APP, which makes it necessary to build and deploy the build package manually on the development server repeatedly during local development and debugging. Several projects, several rounds of testing, quite a bit tired. So think about writing a tool and automatically deploying it to the server after the yarn build package, so that you can omit: open ftp tool - > find the build directory - > find the deployment directory on the server - > backup - > paste the copy file, which is a tedious manual process to improve the efficiency of mo work (yu).
And with this, when jenkins builds automatically, the shell script for the project configuration can also have fewer lines of code.

How to achieve it?

Thought analysis

First, we analyze the results to be achieved.

  1. Execute yarn build

    Individuals like to use yarn, with npm students can replace themselves.
    Assuming that all projects are built on the create react app || Vue cli scaffolding, if you have your own custom scaffolding, keep looking down. In this paper, create react app scaffolding as an example

  2. Tool automatic trigger
  3. Log on to remote server, establish connection and backup original project
  4. copy all files from the local build directory (in this article, the build directory under the project root directory) and upload them to the remote server deployment directory
  5. Deployment complete, output prompt, close remote connection

Secondly, it analyses how to achieve the results:

  1. Writing tools in front-end projects, preferred node.js
  2. Tools need to be triggered automatically after the implementation of yarn build. Here we need to use the hook postbuild in npm scripts. It will do some finishing work after the implementation of build, which can be used to trigger deployment operations. For those who don't know much about npm scripts, you can refer to Ruan Yifeng npm scripts usage guide
  3. To establish and operate a connection with the server, ssh connection is needed, and third-party libraries are recommended. ssh2
  4. When backing up, it's better to add a specific time to the file name. For example: my_app.bak 2019-10-8-14:36:36. For convenience, use a lightweight time library directly moment

Start drying

  1. Install ssh2, moment

    yarn add ssh2 moment

  2. For convenience, create deploy.js in the project root directory. This js file is the automated deployment tool written this time

    deploy.js is not necessarily in the project root directory. If you are familiar with the way node looks for paths, you can put them under the path you specify.

  3. Open the package.json file and add:

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

    At this point, if console. log ('- - deploy test -') is added to deploy.js, the console executes yarn build, you can see that after the completion of the build, yarn run deploy will continue to execute, and the final console output:

    ---deploy test--
  4. According to the ssh2 document, write the code to connect the server, backup files and upload files:
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')
/*********************************************************************************/
/******************************Please manually configure the following contents *******************************************************************************/
/*********************************************************************************/
/**
 * Remote Server Configuration
 * @type {{password: string, port: number, host: string, username: string}}
 */
const server = {
  host: 'xxx.xxx.xxx.xxx',  //Host ip
  port: 22,                //SSH Connection Port
  username: 'xxxx',        //User name
  password: 'xxxxxxx',     //User login password
}
const baseDir = 'my_app'//Project catalogue
const basePath = '/home'//Project Deployment Directory
const bakDirName = baseDir + '.bak' + moment(new Date()).format('YYYY-M-D-HH:mm:ss')//Backup File Name
const buildPath = path.resolve('./build')//Compiled file directories for local projects
/*********************************************************************************/
/**********************************Configuration End***************************************************************************/
/*********************************************************************************/

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)

  // Create a remote directory
  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
  })

  // Upload files
  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(`The server host:            ${server.host}`)
console.log(`Project Folder:            ${baseDir}`)
console.log(`Project deployment and backup directory:  ${basePath}`)
console.log(`Folder name after backup:      ${bakDirName}`)
console.log('--------deploy start--------------')

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

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

Running result example

When using scripts to trigger, after running yarn build, the lifecycle hook postbuild is automatically triggered for deployment. This process first builds the packaged project locally to the configurable buildPath directory, then backs up my_app to my_app.bak2019-10-8-23:06:27 in the remote server xxxx.xxx.xxxxx/home, and finally uploads all the local buildPath directory files to / home/my_app to complete the deployment.

$ yarn run deploy
$ node ./deploy.js
--------deploy config--------------
//Server host: xxx. xxx. xxx. XXX
//Project folder: my_app
//Project deployment and backup directory: / home
//Backup folder name: 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.

epilogue

Project GitHub address: https://github.com/hello-jun/deploy
This tool can also be used independently. After a little modification, it can also be used to automatically deploy react native projects. If you are interested, you can try it on your own.~
Welcome to star, message and issue.
I hope this article will be helpful to you, and I wish you a happy working life.

Posted by mcmuney on Wed, 09 Oct 2019 14:03:45 -0700