likes
comments
collection
share

Study: 自动化部署脚本

作者站长头像
站长
· 阅读数 49

前言(闲谈)

我记得有次面试的时候,被问了一下:说说你对前端工程化的理解

毫无疑问,没有回答上来,哈哈哈。在线下,专门去查询了一下:

前端工程化不是技术而是方向

  • 目的: 提高开发效果,降低成本。
  • 实现方向:模块化,组件化,规范化,自动化

最近,我深刻的感受到自动化存在的必要性。因为把自己的项目部署到服务器上,总会经历以下过程:

pnpm build + 打包文件压缩 + 打开 FinalSheel + 连接服务器 + 上传文件,并解压 + 完成部署

关键还是采用了微前端,多应用的部署,更是增加了步骤的重复性。这些耗时又无意义的操作,是完全没有必要存在的。

那么自动化部署是最好的解决方案。

就想起了公司的一位前同事在项目中写了一个自动部署脚本,就在业余的时间学习了一波(最近公司太忙了,竟然不能上班摸鱼学习,让我...)。

其实为什么不直接搬过来用呢?也许是趁着年轻,还有一股学习劲,学习学习新的知识,总是好的。

上面都是一大堆闲话,可以忽略跳过。

准备工作

针对实现自动化部署脚本,需要安装以下依赖:

  • node-ssh(必须):用于连接远程服务器,远程执行命令。
  • shelljs(必须):用于在 js 文件中自动执行 shell 命令,代替手动在终端中进行操作。
  • zip-local (必须): 用于压缩打包文件,格式为 zip。
  • chalk(可选):美化控制台的打印日志输出。比如:颜色,加粗,背景等。
  • ora (可选): 美化控制台的命令行加载动画库,提升用户体验。

上面三方库的用法,需要自行去研究一下,也需要了解 node 的内置模块: processpathfs 中的某些方法使用。

脚本实现

同事采用的 cjs 的方式,我改造了一下,采用 esm 的形式。vite 项目,使用 cjs 方式,需要修改文件后缀名为 .cjs,不是很习惯。

脚本的实现思路

Study: 自动化部署脚本

配置信息文件

配置信息就不做解释,每台服务器的基本信息,用户信息,配置信息都不相同,那么就需要采用配置化的形式来解决类似问题。

deploy.config.js 配置文件

export default {
  SERVER_PATH: "xxx", // 服务器ip地址
  SSH_USER: "xxx",    // 服务器用户名
  SSH_KEY: "xxx",     // 服务器密码
  PORT: "xx",         // 端口
  SCRIPT: "xxx",      // 打包命令 npm run build 
  DIST: "xxx",        // 执行打包命令后,生成的文件名(build, dist)
  PWD: "xxx",         // 命令执行目录(上一层,用于创建静态文件目录,解压上传文件压缩包等操作)
  PATH: "xxx/yyy",    // 服务器存放静态文件目录
  COMMONDS: [`rm -rf yyy`, `mkdir yyy`], // 上传之前执行的前置命令【先删除原有静态文件(含其内容),再创建静态文件(空)】
}

编写步骤一:读取配置文件

支持配置文件:deploy.config.ts 或者 deploy.config.js

import path from "node:path"
import fs from "node:fs"

// 支持配置文件
const configFile = ["deploy.config.ts", "deploy.config.js"]

async function getConfig() {
  try {
    // 读取根目录下,所有的文件信息,判断是否存在配置文件;不存在则抛错;存在则读取
    const files = fs.readdirSync(process.cwd())
    let _configFile = configFile.find((v) => files.includes(v))
    if (!_configFile) {
      throw "根目录下不存在配置文件:deploy.config.(ts|js)"
    }
    // 读取配置文件
    const _config = await import(
      path.resolve(process.cwd(), `./${_configFile}`)
    )
    return _config.default
  } catch (error) {
    process.exit()
  }
}

const config = await getConfig()

// 打包文件路径
const distDir = path.resolve(process.cwd(), config.DIST || "./dist")
// 打包文件压缩路径
const distZipPath = path.resolve(process.cwd(), `${config.DIST || "dist"}.zip`)

export { config, distDir, distZipPath }

编写步骤二:执行打包命令

执行打包命令脚本

// compileDist.js

const shell = require("shelljs")
const path = require("path")
const { CONFIG } = require("./config.cjs")
const { successLog } = require("./print.cjs")

exports.compileDist = async () => {
  try {
    // 进入本地文件夹
    await shell.cd(path.resolve(process.cwd(), "./"))
    // 执行打包脚本
    await shell.exec(CONFIG.SCRIPT || "npm run build")
    successLog("编译完成")
  } catch (error) {
    throw new Error(`编译失败:${error}`)
  }
}

编写步骤三:打包文件压缩

使用 zip-local 去压缩打包文件,生成压缩文件,格式为 zip。

// zipDist.js

import zipper from "zip-local"
import fs from "node:fs"
import { distDir, distZipPath } from "./config.js"
import { startLog, endLog } from "./utils.js"

export async function zipDist() {
  try {
    // 同步形式的检测压缩包是否已经存在。若存在就删除
    if (fs.existsSync(distZipPath)) {
      fs.unlinkSync(distZipPath)
    }
    startLog("开始打包")
    await zipper.sync.zip(distDir).compress().save(distZipPath)
    endLog("打包完成")
  } catch (error) {
    throw `压缩${distDir}文件夹失败: ${error}`
  }
}

编写步骤四:连接 SSH

通过配置信息,使用 node-ssh 创建的实例对象,调用 connect 函数连接远程服务器。

// connectSSh.js

import ora from "ora"
import fs from "node:fs"
import { config, distZipPath } from "./config.js"
import { startLog, endLog, startChalk, endChalk } from "./utils.js"

export async function connectSSh(SSH) {
  const spinner = ora(startLog("正在连接")).start()
  try {
    SSH.connect({
      host: config.SERVER_PATH,
      username: config.SSH_USER,
      password: config.SSH_KEY,
      port: config.PORT || 22,
    }).then(async () => {
      // 先清除(执行前置命令)
      await runBeforeCommand(SSH)
      // 再上传打包文件
      await uploadFilled(SSH)
    })
    spinner.succeed(endChalk("SSH 连接成功")).stop()
  } catch (error) {
    spinner.stop()
    throw new Error(`SSH 连接失败:${error}`)
  }
}

编写步骤五:执行前置命令

连接远程服务器成功之后。首先要做的事,先执行前置命令,先清空原有的静态资源目录(包含里面的内容),再创建一个空的静态资源文件夹。

// connectSSh.js

async function runCommond(commond, SSH) {
  // 设置当前工作目录为 cwd,然后再执行命令
  await SSH.exec(commond, [], { cwd: config.PWD })
  endLog(`${commond} 命令执行完成`)
}

async function runBeforeCommand(SSH) {
  try {
    const COMMONDS = config.COMMONDS || []
    startLog("执行前置命令")
    for (let i = 0; i < COMMONDS.length; i++) {
      startLog(`执行命令 ${COMMONDS[i]} 开始`)
      await runCommond(COMMONDS[i], SSH)
    }
  } catch (error) {
    throw new Error(`执行cmd失败:${error}`)
  }
}

编写步骤六:上传压缩包并解压

使用 putFile 函数把本地的压缩文件,上传到远程服务器的目录中。

然后使用 unzip 执行,解压文件到上一步创建的空文件夹中。

远程删除压缩包

本地删除压缩包fs.unlinkSync()

// connectSSh.js

async function uploadFilled(SSH) {
  const spinner = ora(startChalk("准备上传文件")).start()
  try {
    startLog(`上传zip至目录 ${config.PWD}`)
    // putFile(source, target)
    // node-ssh 的 putFile 函数是将本地文件上传到远程服务器的方法。该函数可以将本地文件复制到远程服务器的指定目录中
    await SSH.putFile(distZipPath, config.PWD + "/build.zip")
    spinner.text = "完成上传, 开始解压"
    spinner.color = "blue"
    spinner.spinner = {
      interval: 70, //转轮动画每帧之间的时间间隔
      frames: ["✹"],
    }
    await runCommond(`unzip -o ${config.PWD}/build.zip -d ${config.PATH}`, SSH)
    spinner.text = "解压完成,删除压缩包"
    await runCommond(`rm -rf ${config.PWD}/build.zip`, SSH)
    // 删除压缩文件
    fs.unlinkSync(distZipPath)
    spinner.succeed(endChalk("🚀🚀🚀 部署成功")).stop()
    process.exit(0)
  } catch (error) {
    throw new Error(error)
  }
}

编写步骤七:编写工具函数

终端彩色打印

//utils.js

import chalk from "chalk"

const prefixStr = ">>>>>"
// 颜色标记
const startChalk = (info) => chalk.blueBright.bold(`${info}`)
const endChalk = (info) => chalk.green.bold(`${info}`)
const errorChalk = (info) => chalk.red.bold(`${info}`)
// 打印
const startLog = (info) => console.log(`${prefixStr}${startChalk(info)}`)
const endLog = (info) => console.log(`${prefixStr}${endChalk(info)}`)
const errorLog = (info) => console.log(`${prefixStr}${errorChalk(info)}`)

export { startLog, endLog, errorLog, startChalk, endChalk, errorChalk }

编写步骤八:编写入口函数

// index.js

import { NodeSSH } from "node-ssh"
import process from "node:process"
import { compileDist } from "./compileDist.js"
import { zipDist } from "./zipDist.js"
import { connectSSh } from "./connectSSH.js"
import { errorLog } from "./utils.js"

// 捕捉进程异常
process.on("uncaughtException", (err) => {
  errorLog(`系统发生错误了:${err}`)
  process.exit(0)
})

// 执行打包上传命令
async function main() {
  try {
    // 穿件 ssh 实例对象
    const SSH = new NodeSSH()
    // 执行打包命令
    await compileDist()
    // 压缩
    await zipDist()
    // 连接远程服务器,并上传文件部署
    await connectSSh(SSH)
  } catch (error) {
    errorLog(error)
    throw new Error(error)
  }
}

main()

脚本执行

// package.json

{
  "scripts": {
    "dev": "vite",
     // ...
    "deploy": "node ./script/deploy/index.js"
  },
}

执行 pnpm run deploy,就可以自动化部署了。

总结

虽然这次编写脚本是按照同事的思路进行的,但是并不妨碍在其过程中学到了一些新的东西,也完善了同事写的部分美中不足的代码逻辑。当然,也欢迎指教你们认为上面不足的代码之处,虚心接受。

趁年轻,该学习就学习。

转载自:https://juejin.cn/post/7281486769291034661
评论
请登录