告別手動拖拽上傳!本教程將手把手教你如何通過ssh2-sftp-client實現Vue項目打包后自動上傳到服務器,提升部署效率300%。🚀
一、需求場景與解決方案
在Vue項目開發中,每次執行npm run build
后都需要手動將dist目錄上傳到服務器,既耗時又容易出錯。通過ssh2-sftp-client
庫,我們可以實現:
- 打包完成后自動上傳文件到服務器
- 支持覆蓋更新和增量上傳
- 保留文件權限和目錄結構
- 部署過程可視化(進度條顯示)
二、環境準備
確保你的開發環境已安裝:
- Node.js 14+
- Vue CLI創建的項目
- 服務器SSH連接信息(IP、用戶名、密碼/密鑰
三、安裝依賴
安裝核心庫和進度顯示工具:
npm install ssh2-sftp-client progress --save-dev
npm install chalk --save-dev# 或
yarn add ssh2-sftp-client progress -D
?四、安裝依賴
配置package.json
"scripts": {"dev": "vite --mode development","look": "vite --mode production","build": "vite build --mode production","preview": "vite --mode production","deploy": "node deploy.js","build:deploy": "npm run build && npm run deploy"},
? 五、核心代碼
在根目錄上新建deploy.js 文件
import Client from 'ssh2-sftp-client';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs/promises';
import chalk from 'chalk';const server = {host: '',port: 22,username: '',password: '',remoteRoot: '/www/wwwroot'
};// 使用chalk定義顏色主題
const colors = {header: chalk.cyan.bold,success: chalk.green.bold,warning: chalk.yellow.bold,error: chalk.red.bold,file: chalk.blue,progress: chalk.magenta
};const __dirname = path.dirname(fileURLToPath(import.meta.url));
const localPath = path.resolve(__dirname, 'dist');const sftp = new Client();console.log(colors.header('🚀 開始部署操作'));
console.log(colors.header('===================='));
console.log(colors.header(`📡 連接 ${server.username}@${server.host}:${server.port}`));
console.log(colors.header(`📁 本地目錄: ${localPath}`));
console.log(colors.header(`🌐 遠程目錄: ${server.remoteRoot}`));
console.log(colors.header('====================\n'));// 遞歸計算文件總數
async function getTotalFiles(dir) {const entries = await fs.readdir(dir, { withFileTypes: true });let count = 0;for (const entry of entries) {const fullPath = path.join(dir, entry.name);if (entry.isDirectory()) {count += await getTotalFiles(fullPath);} else if (entry.isFile() && !entry.name.includes('.DS_Store')) {count++;}}return count;
}sftp.connect({host: server.host,port: server.port,username: server.username,password: server.password,tryKeyboard: true
}).then(async () => {console.log(colors.success('🔑 認證成功,開始掃描本地文件...'));const totalFiles = await getTotalFiles(localPath);if (totalFiles === 0) {console.log(colors.warning('?? 警告: 本地目錄為空,沒有文件需要上傳'));await sftp.end();return;}console.log(colors.success(`📊 發現 ${totalFiles} 個文件需要上傳\n`));console.log(colors.header('🚚 開始上傳文件:'));console.log(colors.header('------------------------------------'));let uploadedCount = 0;return sftp.uploadDir(localPath, server.remoteRoot, {ticker: (localFile) => {uploadedCount++;const relativePath = path.relative(localPath, localFile);const progress = Math.round((uploadedCount / totalFiles) * 100);console.log(colors.progress(`[${uploadedCount.toString().padStart(3, ' ')}/${totalFiles}]`) +colors.file(` ${relativePath}`) +colors.progress(` (${progress}%)`));},filter: f => !f.includes('.DS_Store')});}).then(() => {console.log('\n' + colors.success('? 所有文件上傳完成!'));console.log(colors.success('🏁 部署成功'));sftp.end();}).catch(err => {console.error('\n' + colors.error('? 嚴重錯誤: ' + err.message));console.error(colors.error('🔍 失敗原因分析:'));if (err.message.includes('connect')) {console.error(colors.error('- 無法連接到服務器,請檢查網絡'));console.error(colors.error('- 防火墻設置可能阻止了連接'));console.error(colors.error('- 服務器可能未運行SSH服務'));} else if (err.message.includes('Authentication')) {console.error(colors.error('- 用戶名或密碼錯誤'));console.error(colors.error('- 服務器可能禁用了密碼登錄'));console.error(colors.error('- 嘗試使用SSH密鑰認證'));} else if (err.message.includes('No such file')) {console.error(colors.error('- 本地文件不存在或路徑錯誤'));console.error(colors.error('- 檢查本地dist目錄是否存在'));}console.error('\n' + colors.error('🛠? 診斷命令:'));console.error(colors.error(`telnet ${server.host} ${server.port}`));console.error(colors.error(`ssh ${server.username}@${server.host}`));if (sftp) sftp.end();process.exit(1);});