为什么写这个?
真相只有一个=>懒!!!
想直接使用的,github 传送门 后面的可以不用看了- -,记得留个star,笔心
目前主要在做iot项目,由于历史原因,平台还存在许多react/vue的纯H5子项目,这些项目又必须调用APP暴露的某些api,使得本地开发调试时不得不重复构建手动将build包部署到开发服务器上。几个项目,几轮转测试下来,颇有些心累。因此想着能不能写个工具,在执行yarn build
构建打包后自动部署到服务器,这样就可以省去:打开ftp工具->寻找构建目录->寻找服务器上部署目录->备份->粘贴复制文件,这套繁琐手动流程,提高工(mo)作(yu)效率。
并且,有了这个后,jenkins自动构建时,项目配置的shell脚本也能少几行代码。
怎么实现?
思路分析
首先,我们先分析下要达到的结果:
-
执行
yarn build
个人喜欢用
yarn
,用npm
的同学可以自己替换。
假设项目都是基于create react app
||Vue cli
脚手架搭建的,如果是自己自定义的脚手架的话,请继续往下看。本文以 create react app脚手架为例 - 工具自动触发
- 登陆远程服务器,建立连接,备份原有项目
- 从本地构建目录(本文为项目根目录下的
build
目录)copy所有文件并上传至远程服务器部署目录 - 部署完成,输出提示,关闭远程连接
其次,分析如何达成结果:
- 前端项目中写工具,首选node.js
- 工具需要在执行
yarn build
以后自动触发,这里我们需要用到npm scripts
中的钩子postbuild
,它会在执行build后做一些收尾工作,正好可以用来触发部署操作。对npm scripts
不太了解的同学,可以参阅《阮一峰 npm scripts 使用指南》 - 与服务器建立连接,并进行操作,需要建立
ssh
连接,推荐使用第三方库ssh2 - 备份时,文件名最好加上具体时间。例如:my_app.bak2019-10-8-14:36:36。为了方便,直接使用一个轻量级时间库moment
开干
-
安装 ssh2 、moment
yarn add ssh2 moment
-
为了方便起见,在项目根目录下新建
deploy.js
。这个js文件就是本次编写的自动化部署工具deploy.js
不一定要在项目根目录下,如果你对node寻找路径方式比较熟悉,可以放在自己指定的路径下面 -
打开
package.json
文件,在scripts
中添加:"postbuild": "yarn run deploy", "deploy": "node ./deploy.js",
此时,如果往
deploy.js
中添加console.log('---deploy test--')
,控制台执行yarn build
,可以看到,在构建完成后,会继续执行yarn run deploy
,最终控制台输出:---deploy test--
- 根据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。
希望本文对各位有所帮助,祝工作生活愉快!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。