告别手动拖拽上传!本教程将手把手教你如何通过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);
});