基于Vue?+?Node.js自動發(fā)布服務腳本的完整流程
前言
基于 Vue 和 Node.js 的自動化發(fā)布服務腳本實現(xiàn)方案,該方案主要依賴 node-ssh(用于服務器 SSH 連接)和 archiver(用于文件壓縮打包)兩大核心模塊。通過與 package.json 中配置的多環(huán)境部署腳本協(xié)同工作,在 deploy 目錄下構建完整的發(fā)布流程。以下將詳細介紹腳本開發(fā)、環(huán)境配置及使用說明,確保完全符合您的項目需求。
一、環(huán)境準備:依賴安裝
請在 Vue 項目根目錄執(zhí)行以下命令安裝必要依賴(包括 rimraf 清理工具,該依賴已在您的 package.json 中配置,建議一并確認安裝狀態(tài)):
# 核心依賴:ssh連接、壓縮打包 npm i node-ssh archiver -D # 清理依賴(若未安裝) npm i rimraf -D # ssh npm i node-ssh -D
二、項目目錄結構
在 Vue 項目根目錄新建 deploy 文件夾,最終目錄結構如下(新增文件已標注):

三、編寫 deploy 文件夾下的核心文件
deploy/index.js :部署入口文件 核心邏輯:解析命令行的環(huán)境參數(shù) → 執(zhí)行 Vue 打包 → 壓縮 dist 目錄為 zip 包 → SSH 連接服務器 → 上傳壓縮包并解壓 → 清理臨時文件 → 完成部署,全程自動化,無需手動操作:
* 自動化部署腳本
* @description 實現(xiàn)項目打包、壓縮、上傳、備份、解壓等自動化部署功能
*/
const { NodeSSH } = require("node-ssh");
const archiver = require("archiver");
const fs = require("fs");
const path = require("path");
const { exec } = require("child_process");
const util = require("util");
const execPromise = util.promisify(exec);
/**
* 獲取環(huán)境配置
* @returns {Object} 配置對象
*/
function getConfig() {
// 從命令行參數(shù)獲取環(huán)境,例如: node deploy/index.js --env=test
const args = process.argv.slice(2);
let env = "prod"; // 默認生產環(huán)境
// 解析命令行參數(shù)
for (const arg of args) {
if (arg.startsWith("--env=")) {
env = arg.split("=")[1];
}
}
// 如果有環(huán)境變量 DEPLOY_ENV,優(yōu)先使用
if (process.env.DEPLOY_ENV) {
env = process.env.DEPLOY_ENV;
}
// 處理 production 別名
if (env === "production") {
env = "prod";
}
// 直接根據(jù)環(huán)境名稱構建配置文件路徑
const configFile = `./config.${env}.js`;
const configPath = path.join(__dirname, configFile);
// 檢查配置文件是否存在
if (!fs.existsSync(configPath)) {
throw new Error(
`配置文件不存在: ${configFile}\n` +
`請創(chuàng)建 deploy/config.${env}.js 文件,或使用已有的環(huán)境配置。\n` +
`可用環(huán)境示例: test, prod, xj, mj`
);
}
console.log(`\x1b[36m[INFO]\x1b[0m 使用配置環(huán)境: ${env}`);
console.log(`\x1b[36m[INFO]\x1b[0m 配置文件: ${configFile}`);
return require(configFile);
}
const config = getConfig();
const ssh = new NodeSSH();
/**
* 輸出帶顏色的日志
*/
const log = {
info: (msg) => console.log(`\x1b[36m[INFO]\x1b[0m ${msg}`),
success: (msg) => console.log(`\x1b[32m[SUCCESS]\x1b[0m ${msg}`),
error: (msg) => console.log(`\x1b[31m[ERROR]\x1b[0m ${msg}`),
warning: (msg) => console.log(`\x1b[33m[WARNING]\x1b[0m ${msg}`),
};
/**
* 步驟1: 執(zhí)行項目打包
* @returns {Promise<void>}
*/
async function buildProject() {
log.info("開始執(zhí)行項目打包...");
try {
const { stdout, stderr } = await execPromise(config.build.command);
if (stderr && !stderr.includes("warning")) {
log.warning(`構建警告: ${stderr}`);
}
log.success("項目打包完成!");
return true;
} catch (error) {
log.error(`項目打包失敗: ${error.message}`);
throw error;
}
}
/**
* 步驟2: 壓縮dist文件夾為zip
* @returns {Promise<string>} 返回zip文件路徑
*/
async function compressDistToZip() {
log.info("開始壓縮dist文件夾...");
const { localDistPath, localZipName } = config.deploy;
const zipPath = path.join(process.cwd(), localZipName);
// 如果已存在zip文件,先刪除
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
}
// 檢查 dist 目錄是否存在
if (!fs.existsSync(localDistPath)) {
throw new Error(`dist目錄不存在: ${localDistPath}`);
}
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipPath);
const archive = archiver("zip", {
zlib: { level: 9 }, // 壓縮級別
});
let fileCount = 0;
output.on("close", () => {
const size = (archive.pointer() / 1024 / 1024).toFixed(2);
log.success(`壓縮完成! 文件大小: ${size} MB, 文件數(shù): ${fileCount}`);
log.info(`壓縮包路徑: ${zipPath}`);
resolve(zipPath);
});
archive.on("error", (err) => {
log.error(`壓縮失敗: ${err.message}`);
reject(err);
});
archive.on("warning", (err) => {
if (err.code === "ENOENT") {
log.warning(`壓縮警告: ${err.message}`);
} else {
reject(err);
}
});
// 監(jiān)聽文件添加事件
archive.on("entry", (entry) => {
fileCount++;
});
archive.pipe(output);
// 將dist文件夾內容添加到壓縮包根目錄
// false 參數(shù)表示不包含 dist 文件夾本身,直接將其內容放在 zip 根目錄
archive.directory(localDistPath, false);
archive.finalize();
});
}
/**
* 步驟3: 連接SSH服務器
* @returns {Promise<void>}
*/
async function connectSSH() {
log.info("正在連接SSH服務器...");
try {
await ssh.connect({
host: config.server.host,
port: config.server.port,
username: config.server.username,
password: config.server.password,
});
log.success(`成功連接到服務器: ${config.server.host}`);
} catch (error) {
log.error(`SSH連接失敗: ${error.message}`);
throw error;
}
}
/**
* 步驟4: 備份服務器上的dist目錄
* @returns {Promise<void>}
*/
async function backupRemoteDist() {
log.info("正在備份服務器上的dist目錄...");
const { remoteDir, remoteDist, backupDir } = config.deploy;
const remoteDistPath = `${remoteDir}/${remoteDist}`;
// 生成時間戳:格式如 20260204_153045
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const timestamp = `${year}${month}${day}_${hours}${minutes}${seconds}`;
const backupPath = `${backupDir}/${remoteDist}_backup_${timestamp}`;
log.info(`備份時間: ${year}-${month}-${day} ${hours}:${minutes}:${seconds}`);
try {
// 檢查dist目錄是否存在
const checkCmd = `[ -d "${remoteDistPath}" ] && echo "exists" || echo "not_exists"`;
const checkResult = await ssh.execCommand(checkCmd);
if (checkResult.stdout.trim() === "exists") {
// 創(chuàng)建備份目錄
await ssh.execCommand(`mkdir -p ${backupDir}`);
// 備份dist目錄
const backupCmd = `cp -r ${remoteDistPath} ${backupPath}`;
await ssh.execCommand(backupCmd);
log.success(`備份完成: ${backupPath}`);
} else {
log.warning("服務器上不存在dist目錄,跳過備份");
}
} catch (error) {
log.error(`備份失敗: ${error.message}`);
throw error;
}
}
/**
* 步驟5: 上傳zip文件到服務器(已合并到步驟6)
* @deprecated 此步驟已合并
*/
/**
* 步驟6: 上傳zip文件到服務器
* @param {string} zipPath 本地zip文件路徑
* @returns {Promise<void>}
*/
async function uploadZipToServer(zipPath) {
log.info("正在上傳dist.zip到服務器...");
const { remoteDir, localZipName } = config.deploy;
const remoteZipPath = `${remoteDir}/${localZipName}`;
try {
// 獲取文件大小
const stats = fs.statSync(zipPath);
const fileSize = stats.size;
const fileSizeMB = (fileSize / 1024 / 1024).toFixed(2);
log.info(`文件大小: ${fileSizeMB} MB`);
// 上傳文件并顯示進度(提高并發(fā)數(shù)加速上傳)
await ssh.putFile(zipPath, remoteZipPath, null, {
concurrency: 20, // 提高并發(fā)數(shù),加速上傳
chunkSize: 32768, // 32KB 塊大小
step: (transferred, chunk, total) => {
const percent = ((transferred / total) * 100).toFixed(2);
const transferredMB = (transferred / 1024 / 1024).toFixed(2);
const totalMB = (total / 1024 / 1024).toFixed(2);
const speed = (chunk / 1024 / 1024).toFixed(2);
// 使用\r回到行首,實現(xiàn)進度條效果
process.stdout.write(
`\r\x1b[36m[INFO]\x1b[0m 上傳進度: ${percent}% (${transferredMB}MB / ${totalMB}MB) 速度: ${speed}MB/s`
);
},
});
// 上傳完成后換行
console.log("");
log.success(`上傳完成: ${remoteZipPath}`);
} catch (error) {
console.log(""); // 確保錯誤信息另起一行
log.error(`上傳失敗: ${error.message}`);
throw error;
}
}
/**
* 步驟7: 在服務器上解壓zip文件并替換dist
* @returns {Promise<void>}
*/
async function unzipAndReplaceDist() {
log.info("正在解壓dist.zip并準備替換...");
const { remoteDir, localZipName, remoteDist } = config.deploy;
const remoteZipPath = `${remoteDir}/${localZipName}`;
const remoteDistPath = `${remoteDir}/${remoteDist}`;
try {
// 檢查zip文件是否存在
const checkZipCmd = `[ -f "${remoteZipPath}" ] && echo "exists" || echo "not_exists"`;
const checkZipResult = await ssh.execCommand(checkZipCmd);
if (checkZipResult.stdout.trim() !== "exists") {
throw new Error("服務器上的zip文件不存在");
}
log.info("正在檢查zip文件內容...");
const listZipCmd = `unzip -l ${remoteZipPath} | head -20`;
const listResult = await ssh.execCommand(listZipCmd);
log.info("zip文件內容預覽:");
console.log(listResult.stdout);
// 創(chuàng)建臨時目錄并解壓
const tempDir = `${remoteDir}/temp_dist_${Date.now()}`;
log.info(`解壓到臨時目錄: ${tempDir}`);
// 解壓命令:使用 -o 覆蓋已存在文件
const unzipCmd = `mkdir -p ${tempDir} && unzip -o -q ${remoteZipPath} -d ${tempDir}`;
const unzipResult = await ssh.execCommand(unzipCmd);
if (unzipResult.code !== 0) {
log.error(`解壓錯誤: ${unzipResult.stderr}`);
// 清理臨時目錄
await ssh.execCommand(`rm -rf ${tempDir}`);
throw new Error(`解壓失敗: ${unzipResult.stderr}`);
}
// 檢查解壓后的內容和文件數(shù)量
const checkContentCmd = `ls -lah ${tempDir} | head -20 && echo "--- 文件統(tǒng)計 ---" && find ${tempDir} -type f | wc -l`;
const contentResult = await ssh.execCommand(checkContentCmd);
log.info("解壓后的內容:");
console.log(contentResult.stdout);
// 刪除舊dist并用臨時目錄替換(使用原子操作)
log.info(`正在替換目標目錄: ${remoteDistPath}`);
// 方案:先刪除舊的,再重命名新的(更可靠)
const deleteOldCmd = `rm -rf ${remoteDistPath}`;
const deleteResult = await ssh.execCommand(deleteOldCmd);
if (deleteResult.code !== 0 && deleteResult.stderr) {
log.warning(`刪除舊目錄警告: ${deleteResult.stderr}`);
}
const moveCmd = `mv ${tempDir} ${remoteDistPath}`;
const moveResult = await ssh.execCommand(moveCmd);
if (moveResult.code !== 0) {
log.error(`移動失敗: ${moveResult.stderr}`);
throw new Error(`替換文件失敗: ${moveResult.stderr}`);
}
log.success("替換完成");
// 驗證最終結果 - 顯示詳細信息
log.info("正在驗證部署結果...");
const verifyCmd = `
echo "=== 目錄內容 ===" &&
ls -lah ${remoteDistPath} | head -20 &&
echo "" &&
echo "=== 文件統(tǒng)計 ===" &&
echo "總文件數(shù): $(find ${remoteDistPath} -type f | wc -l)" &&
echo "總目錄數(shù): $(find ${remoteDistPath} -type d | wc -l)" &&
echo "總大小: $(du -sh ${remoteDistPath} | cut -f1)" &&
echo "" &&
echo "=== index.html 信息 ===" &&
ls -lh ${remoteDistPath}/index.html 2>/dev/null || echo "index.html 不存在"
`;
const verifyResult = await ssh.execCommand(verifyCmd);
console.log(verifyResult.stdout);
if (verifyResult.code !== 0) {
log.warning("驗證過程中出現(xiàn)警告,但部署已完成");
}
// 刪除zip文件
await ssh.execCommand(`rm -f ${remoteZipPath}`);
log.success("清理臨時文件完成");
} catch (error) {
log.error(`操作失敗: ${error.message}`);
// 清理可能的臨時目錄
await ssh.execCommand(`rm -rf ${remoteDir}/temp_dist_*`);
throw error;
}
}
/**
* 步驟8: 清理本地zip文件
* @param {string} zipPath 本地zip文件路徑
* @returns {void}
*/
function cleanupLocalZip(zipPath) {
log.info("正在清理本地臨時文件...");
try {
if (fs.existsSync(zipPath)) {
fs.unlinkSync(zipPath);
log.success("本地臨時文件清理完成");
}
} catch (error) {
log.warning(`清理本地文件失敗: ${error.message}`);
}
}
/**
* 主部署流程
*/
async function deploy() {
const startTime = Date.now();
console.log("\n");
log.info("========================================");
log.info(" 開始自動化部署流程");
log.info("========================================");
log.info(`目標服務器: ${config.server.host}`);
log.info(`部署目錄: ${config.deploy.remoteDir}/${config.deploy.remoteDist}`);
log.info(`構建模式: ${config.build.mode}`);
log.info("========================================\n");
let zipPath = "";
try {
// 1. 打包項目
await buildProject();
console.log("\n");
// 2. 壓縮dist
zipPath = await compressDistToZip();
console.log("\n");
// 3. 連接SSH
await connectSSH();
console.log("\n");
// 4. 備份服務器dist
await backupRemoteDist();
console.log("\n");
// 5. 上傳zip
await uploadZipToServer(zipPath);
console.log("\n");
// 6. 解壓zip并替換dist
await unzipAndReplaceDist();
console.log("\n");
// 8. 清理本地zip
cleanupLocalZip(zipPath);
console.log("\n");
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
log.info("========================================");
log.success(` 部署成功! 耗時: ${duration}秒`);
log.info("========================================\n");
} catch (error) {
log.error(`\n部署失敗: ${error.message}\n`);
// 清理本地文件
if (zipPath) {
cleanupLocalZip(zipPath);
}
process.exit(1);
} finally {
// 斷開SSH連接
if (ssh.isConnected()) {
ssh.dispose();
log.info("SSH連接已關閉");
}
}
}
// 執(zhí)行部署
if (require.main === module) {
deploy();
}
module.exports = { deploy };
- deploy/config.XX.js :多環(huán)境服務器配置文件
集中管理 prod/xj/mj/test 四個環(huán)境的SSH 連接信息、服務器部署目錄,后續(xù)修改環(huán)境配置僅需改此文件,按需替換你的實際配置:
* 測試環(huán)境部署配置
* @description 測試服務器配置
*/
module.exports = {
// 服務器配置
server: {
host: "1.1.1.1",
port: 22,
username: "admin",
password: "123",
},
// 部署配置
deploy: {
localDistPath: "./dist",
localZipName: "dist.zip",
remoteDir: "/home/web",
remoteDist: "dist",
backupDir: "/home/web/backup",
},
// 構建配置
build: {
command: "npm run build:test",
mode: "test",
},
};
四、package.json 腳本說明(你的原有配置,無需修改)
你的 package.json 中已配置好多環(huán)境啟動、打包、部署腳本,對應關系如下,直接執(zhí)行即可:
// 本地啟動腳本(按環(huán)境區(qū)分,無需修改) "serve": "vue-cli-service serve", "dev": "vue-cli-service serve", "test": "vue-cli-service serve --mode test", "start": "npm run dev", // 核心部署腳本:執(zhí)行后自動打包+發(fā)布到對應服務器 "deploy:prod": "node deploy/index.js --env=prod", "deploy:xj": "node deploy/index.js --env=xj", "deploy:mj": "node deploy/index.js --env=mj", "deploy:test": "node deploy/index.js --env=test", }
五、使用說明
修改配置:先修改 deploy/config.js 中四個環(huán)境的服務器 IP、賬號、密碼 、部署目錄,確保信息正確;
執(zhí)行部署:在項目根目錄執(zhí)行對應環(huán)境的部署命令,全程自動化,示例:
npm run deploy:prod # 部署到測試環(huán)境 npm run deploy:test
部署流程:執(zhí)行命令后,腳本會自動完成「環(huán)境打包 → 壓縮 dist → SSH 連服務器 → 上傳解壓 → 清理臨時文件」,全程控制臺輸出彩色日志,成功 / 失敗一目了然。
六、關鍵注意事項
服務器準備:
- 安裝unzip工具(CentOS執(zhí)行yum install unzip -y,Ubuntu執(zhí)行apt install unzip -y)
- 確保部署目錄具有讀寫權限(推薦使用root賬戶或授權對應賬號)
打包配置:
- 默認test環(huán)境執(zhí)行npm run build:test
- 其他環(huán)境執(zhí)行npm run build
日志與錯誤處理:
- 任一環(huán)節(jié)失敗時自動終止流程,并輸出紅色錯誤提示
- 自動清理臨時文件并斷開SSH連接,確保無資源殘留
部署說明:
- 安裝核心依賴:npm i node-ssh archiver rimraf -D
主要配置文件:
- deploy/config.xx.js(多環(huán)境服務器配置)
- deploy/index.js(自動化部署邏輯)
- 執(zhí)行命令:npm run deploy:xxx,自動完成「打包→壓縮→上傳→解壓」全流程服務器準備:
- 確保服務器安裝了 unzip 命令(用于解壓,若未安裝執(zhí)行 yum install unzip -y(CentOS)或 apt install unzip -y(Ubuntu));
- 服務器部署目錄需有讀寫權限(建議用 root 或賦予對應賬號權限);
- 打包匹配:腳本中默認 test 環(huán)境執(zhí)行 npm run build:test,其他環(huán)境執(zhí)行 npm run build,若 xj/mj 需單獨打包,可在 buildProject 函數(shù)中新增判斷;
- 日志輸出:腳本使用 stdio: “inherit” 讓打包、解壓的日志直接輸出到控制臺,方便排查問題;
- 錯誤處理:任意步驟失敗都會終止腳本,并輸出紅色錯誤信息,同時清理臨時文件、斷開 SSH 連接,避免資源殘留。
七、總結
- 核心依賴為 node-ssh(SSH 連接)和 archiver(壓縮),需先執(zhí)行 npm i node-ssh archiver rimraf -D 安裝;
- 核心文件是 deploy/config.js(多環(huán)境服務器配置)和 deploy/index.js(自動化部署邏輯),無需修改原有 package.json;
- 部署命令直接用你原有配置的 npm run deploy:xxx,全程自動化完成「打包→壓縮→上傳→解壓」,無需手動操作服務器;
到此這篇關于基于Vue+Node.js自動發(fā)布服務腳本的文章就介紹到這了,更多相關Vue Node.js自動發(fā)布服務腳本內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
更強大的vue ssr實現(xiàn)預取數(shù)據(jù)的方式
這篇文章主要介紹了更強大的 vue ssr 預取數(shù)據(jù)的方式,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07
el-select 數(shù)據(jù)回顯,只顯示value不顯示lable問題
這篇文章主要介紹了el-select 數(shù)據(jù)回顯,只顯示value不顯示lable問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09
在vue中使用screenfull?依賴,實現(xiàn)全屏組件方式
這篇文章主要介紹了在vue中使用screenfull?依賴,實現(xiàn)全屏組件方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
Vue實現(xiàn)Header漸隱漸現(xiàn)效果的實例代碼
這篇文章主要介紹了Vue實現(xiàn)Header漸隱漸現(xiàn)效果,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11
vue開發(fā)之LogicFlow自定義業(yè)務節(jié)點
這篇文章主要為大家介紹了vue開發(fā)之LogicFlow自定義業(yè)務節(jié)點,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-01-01

