想直接使用的,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导致密码泄露,登陆账号权限最好也不是超级管理员。
使用截图: