从零开始 Node实现前端自动化部署

从零开始

更新:
🎉现已支持前端docker自动化部署,详情请见前端docker自动化部署

🎉现已支持添加多个配置信息,自动化部署时支持选择配置信息运行

🎉现已支修改服务器连接端口,支持ssh私钥及解密密码连接(ps:不使用此方法时,请注释privateKey)

🎉现已更新模块引用逻辑,远端备份时间格式改为 yyyy-MM-dd_HH:mm:ss

效果展示

1. 待部署工程本地完成打包构建

本地打包构建目录

2. 确定远端部署目录及发布文件夹

远端部署目录

3. 修改配置

修改配置文件

4. 运行自动化部署

选择配置信息
自动化部署

5. 查看远端效果

远端部署目录

6. 再次部署 原目录已备份(开启远端备份生效)

远端部署目录

前言

前端项目部署时,nginx配置完成后,只需将打包后的文件上传至服务器指定目录下即可。

一般使用以下方式完成:

  • xshell 等命令行工具上传
  • ftp 等可视化工具上传
  • jenkins 等自动化部署服务

对于简单前端项目,频繁部署时,xshellftp两种方式较为繁琐,而jenkins 等自动化部署服务需要提前安装软件、并熟悉配置流程。
因此希望借助本地 node 服务实现对前端打包后文件的上传工作,既不需要服务器额外安装程序,还可以帮助我们实现快速上传部署,更能帮助我们深入了解 node

开始

1. 明确需求

进行开发前需要首先明确需求,根据常见的前端部署流程总结为以下过程:

前端部署流程

根据部署流程明确自动化部署的需求:

明确需求

2. 开发前准备

2.1 导入依赖模块

由于需要实现文件压缩、及连接远程服务器、实现远程命令调用,因此至少需要以下模块:

  • ssh 模块(可实现连接服务器、命令调用等常见操作)
  • 文件压缩 模块(可实现 .zip 等常见压缩文件的本地打包)
  • 命令行选择 模块(可实现对多配置项文件进行选择和使用)

查找资料,最终选择 node-ssharchiverinquirer 分别实现上述功能。

# 安装依赖
npm i node-ssh --save
npm i archiver --save
npm i inquirer --save

2.2 如何实现规范

为实现需求中的 解耦合理逻辑清晰/灵活 ,需要关注整体程序逻辑,这里选择 封装 相关功能实现,并在 主程序 中自由调度(可灵活 调用/关闭/修改 相关功能),并对于当前所执行功能给与提示,以保证功能实现的 完整性异常提示

因为 文件压缩文件上传执行远端命令 等存在异步过程,因此需要明确功能完成的顺序,即 应在前置任务完成的回调中开启当前任务,为实现控制异步过程和代码逻辑清晰,这里选择使用 ES6 的 Promise 结合 ES7的语法糖 async awiat 实现逻辑流程控制。

到这里就完成了对程序功能构建的梳理工作,下面进入项目实现。

3. 功能实现

工程目录预览
这里先展示最终结果的工程目录,以供参考:

工程目录

  • node_modules
  • utils
    • compressFile.js【压缩本地文件】
    • handleCommand.js【调用远端命令】
    • handleTime.js【时间处理】
    • helper.js【部署项目选择提示】
    • ssh.js【连接远端务器】
    • uploadFile.js【上传本地文件】
  • app.js【主程序】
  • config.js【配置文件】
  • dist.zip【压缩后的文件】(当前版本不会自动删除)
  • package.json
  • README.md【项目介绍】

3.1 压缩本地文件

compressFile 接收 需要压缩的目录打包生成文件,传入后实现本地文件压缩。

compressFile.js 参考代码

// compressFile.js
const fs = require('fs')
const archiver = require('archiver')

function compressFile (targetDir, localFile) {
  return new Promise((resolve, reject)=>{
    console.log('1-正在压缩文件...')
    let output = fs.createWriteStream(localFile) // 创建文件写入流
    const archive = archiver('zip', {
      zlib: { level: 9 } // 设置压缩等级
    })
    output.on('close', () => {
      resolve(
        console.log('2-压缩完成!共计 ' + (archive.pointer() / 1024 /1024).toFixed(3) + 'MB')
      )
    }).on('error', (err) => {
      reject(console.error('压缩失败', err))
    })
    archive.pipe(output) // 管道存档数据到文件
    archive.directory(targetDir, 'dist') // 存储目标文件并重命名
    archive.finalize() // 完成文件追加 确保写入流完成
  })
}

module.exports = compressFile

3.2 连接远端服务器

connectServe 接收远端ip用户名密码等信息,完成远端服务器连接,具体配置参考 config.js

ssh.js 参考代码

// ssh.js
const node_ssh = require('node-ssh')
const ssh = new node_ssh()

function connectServe (sshInfo) {
  return new Promise((resolve, reject) => {
    ssh.connect({ ...sshInfo }).then(() => {
      resolve(console.log('3-' + sshInfo.host + ' 连接成功'))
    }).catch((err) => {
      reject(console.error('3-' + sshInfo.host + ' 连接失败', err))
    })
  })
}

module.exports = connectServe

3.3 远端执行命令

runCommand 接收 需执行的命令执行命令的远端路径,这里将其单独拆分,既方便 主程序 的单独调用,也方便 文件上传 等功能的模块封装,达到 解耦 的效果。

handleCommand.js 参考代码

// handleCommand.js
// run linux shell(ssh对象、shell指令、执行路径)
function runCommand (ssh, command, path) {
  return new Promise((resolve, reject) => {
    ssh.execCommand(command, {
      cwd: path
    }).then((res) => {
      if (res.stderr) {
        reject(console.error('命令执行发生错误:' + res.stderr))
        process.exit()
      } else {
        resolve(console.log(command + ' 执行完成!'))
      }
    })
  })
}

module.exports = runCommand

3.4 文件上传

uploadFile 接收 系统配置参数待上传的本地文件 ,完成本地文件上传至指定服务器目录,这里还引入 handleCommand.jshandleTime.js ,根据 openBackUp 是否开启远端备份,完成对存在解压后同名目录的处理。具体配置参考 config.js

uploadFile.js 参考代码

// uploadFile.js
const runCommand = require ('./handleCommand')
const getCurrentTime = require ('./handleTime')

// 文件上传(ssh对象、配置信息、本地待上传文件)
async function uploadFile (ssh, config, localFile) {
  return new Promise((resolve, reject) => {
    console.log('4-开始文件上传')
    handleSourceFile(ssh, config)
    ssh.putFile(localFile, config.deployDir + config.targetFile).then(async () => {
      resolve(console.log('5-文件上传完成'))
    }, (err) => {
      reject(console.error('5-上传失败!', err))
    })
  })
}

// 处理源文件(ssh对象、配置信息)
async function handleSourceFile (ssh, config) {
  if (config.openBackUp) {
    console.log('已开启远端备份!')
    await runCommand(
      ssh,
      `
      if [ -d ${config.releaseDir} ];
      then mv ${config.releaseDir} ${config.releaseDir}_${getCurrentTime()}
      fi
      `,
      config.deployDir)
  } else {
    console.log('提醒:未开启远端备份!')
    await runCommand(
      ssh,
      `
      if [ -d ${config.releaseDir} ];
      then mv ${config.releaseDir} /tmp/${config.releaseDir}_${getCurrentTime()}
      fi
      `,
      config.deployDir)
  }
}

module.exports = uploadFile

3.5 时间处理

getCurrentTime 获取并返回当前时间,远端远程备份时使用。

handleTime.js 参考代码

// 获取当前时间
function getCurrentTime () {
  const date = new Date
  const yyyy = date.getFullYear()
  const MM = coverEachUnit(date.getMonth() + 1)
  const dd = coverEachUnit(date.getDate())
  const HH = coverEachUnit(date.getHours())
  const mm = coverEachUnit(date.getMinutes())
  const ss = coverEachUnit(date.getSeconds())
  return `${yyyy}-${MM}-${dd}_${HH}:${mm}:${ss}`
}

// 转换时间中一位至两位
function coverEachUnit (val) {
  return val < 10 ? '0' + val : val
}

module.exports = getCurrentTime

3.6 主程序

当所有功能模块封装完成后,其中 异步流程 均使用 Promise 处理,这时结合 async awiat 实现,既保证了功能实现的顺序,也使得功能组合变得更加简洁、优雅。

main 函数中通关功能组合实现自动化部署的流程,后续增加其他功能实现,主程序 中引入、组合即可完成升级。

app.js 参考代码

const config = require ('./config')
const helper = require ('./utils/helper')
const compressFile = require ('./utils/compressFile')
const sshServer = require ('./utils/ssh')
const uploadFile = require ('./utils/uploadFile')
const runCommand = require ('./utils/handleCommand')

// 主程序(可单独执行)
async function main () {
  try {
    console.log('请确保文件解压后为dist目录!!!')
    const SELECT_CONFIG = (await helper(config)).value // 所选部署项目的配置信息
    console.log('您选择了部署 ' + SELECT_CONFIG.name)
    const localFile =  __dirname + '/' + SELECT_CONFIG.targetFile // 待上传本地文件
    SELECT_CONFIG.openCompress ? await compressFile(SELECT_CONFIG.targetDir, localFile) : '' //压缩
    await sshServer.connectServe(SELECT_CONFIG.ssh) // 连接
    await uploadFile(sshServer.ssh, SELECT_CONFIG, localFile) // 上传
    await runCommand(sshServer.ssh, 'unzip ' + SELECT_CONFIG.targetFile, SELECT_CONFIG.deployDir) // 解压
    await runCommand(sshServer.ssh, 'mv dist ' + SELECT_CONFIG.releaseDir, SELECT_CONFIG.deployDir) // 修改文件名称
    await runCommand(sshServer.ssh, 'rm -f ' + SELECT_CONFIG.targetFile, SELECT_CONFIG.deployDir) // 删除
  } catch (err) {
    console.log('部署过程出现错误!', err)
  } finally {
    process.exit()
  }
}

// run main
main()

3.7 配置文件

为方便前端自动化部署,这里抽离关键信息,生成配置文件。
用户只需修改配置文件即可实现 自动化部署

config.js 参考代码

/*
config.js
说明:
  请确保解压后的文件目录为dist
  ssh: 连接服务器用户信息
  targetDir: 需要压缩的文件目录(启用本地压缩后生效)
  targetFile: 指定上传文件名称(config.js同级目录)
  openCompress: 关闭后,将跳过本地文件压缩,直接上传同级目录下指定文件
  openBackUp: 开启后,若远端存在相同目录,则会修改原始目录名称,不会直接覆盖
  deployDir: 指定远端部署地址
  releaseDir: 指定远端部署地址下的发布目录名称
更新:
  🎉现已支持添加多个配置信息,自动化部署时支持选择配置信息运行
  🎉现已支修改服务器连接端口,支持ssh私钥及解密密码连接(ps:不使用此方法时,请注释privateKey)
  🎉现已更新模块引用逻辑,远端备份时间格式改为 `yyyy-MM-dd_HH:mm:ss`
  */

const config = [
  {
    name: '项目A-dev',
    ssh: {
      host: '192.168.0.110',
      port: 22,
      username: 'root',
      password: 'root',
      // privateKey: 'E:/id_rsa', // ssh私钥(不使用此方法时请勿填写, 注释即可)
      passphrase: '123456' // ssh私钥对应解密密码(不存在设为''即可)
    },
    targetDir: 'E:/private/my-vue-cli/dist', // 目标压缩目录(可使用相对地址)
    targetFile: 'dist.zip', // 目标文件
    openCompress: true, // 是否开启本地压缩
    openBackUp: true, // 是否开启远端备份
    deployDir: '/home/node_test' + '/', // 远端目录
    releaseDir: 'web' // 发布目录
  },
  {
    name: '项目A-prod',
    ssh: {
      host: '192.168.0.110',
      port: 22,
      username: 'root',
      password: 'root',
      privateKey: 'E:/id_rsa', // ssh私钥(不使用此方法时请勿填写, 注释即可)
      passphrase: '123456' // ssh私钥对应解密密码(不存在设为''即可)
    },
    targetDir: 'E:/private/my-vue-cli/dist', // 目标压缩目录(可使用相对地址)
    targetFile: 'dist.zip', // 目标文件
    openCompress: true, // 是否开启本地压缩
    openBackUp: true, // 是否开启远端备份
    deployDir: '/home/node_test' + '/', // 远端目录
    releaseDir: 'web2' // 发布目录
  }
]

module.exports = config

使用

拉取源码、安装依赖、修改配置文件、运行即可

npm install
npm run deploy

🎉该项目已开源至 github 欢迎下载使用 后续会完善更多功能 🎉
源码及项目说明

Tip: 喜欢的话别忘记 star 哦😘,有疑问🧐欢迎提出 issues ,积极交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容