Study: 自动化部署脚本
前言(闲谈)
我记得有次面试的时候,被问了一下:说说你对前端工程化的理解?
毫无疑问,没有回答上来,哈哈哈。在线下,专门去查询了一下:
前端工程化不是技术而是方向。
- 目的: 提高开发效果,降低成本。
- 实现方向:模块化,组件化,规范化,自动化
最近,我深刻的感受到自动化存在的必要性。因为把自己的项目部署到服务器上,总会经历以下过程:
pnpm build
+ 打包文件压缩
+ 打开 FinalSheel
+ 连接服务器
+ 上传文件,并解压
+ 完成部署
关键还是采用了微前端,多应用的部署,更是增加了步骤的重复性。这些耗时又无意义的操作,是完全没有必要存在的。
那么自动化部署是最好的解决方案。
就想起了公司的一位前同事在项目中写了一个自动部署脚本,就在业余的时间学习了一波(最近公司太忙了,竟然不能上班摸鱼学习,让我...)。
其实为什么不直接搬过来用呢?也许是趁着年轻,还有一股学习劲,学习学习新的知识,总是好的。
上面都是一大堆闲话,可以忽略跳过。
准备工作
针对实现自动化部署脚本,需要安装以下依赖:
node-ssh
(必须):用于连接远程服务器,远程执行命令。shelljs
(必须):用于在 js 文件中自动执行 shell 命令,代替手动在终端中进行操作。zip-local
(必须): 用于压缩打包文件,格式为 zip。chalk
(可选):美化控制台的打印日志输出。比如:颜色,加粗,背景等。ora
(可选): 美化控制台的命令行加载动画库,提升用户体验。
上面三方库的用法,需要自行去研究一下,也需要了解 node 的内置模块: process
,path
,fs
中的某些方法使用。
脚本实现
同事采用的 cjs 的方式,我改造了一下,采用 esm 的形式。vite 项目,使用 cjs 方式,需要修改文件后缀名为
.cjs
,不是很习惯。
脚本的实现思路

配置信息文件
配置信息就不做解释,每台服务器的基本信息,用户信息,配置信息都不相同,那么就需要采用配置化的形式来解决类似问题。
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