摘要:最近我参与了几个前端手动部署机器的项目,部署流程长期依赖人工操作:从本地打包构建,到登录服务器、定位目录,再到上传压缩包、解压缩完成部署 —— 整套流程步骤繁琐且重复,不仅效率低下,还容易因手动操作出现疏漏。闲暇时,我基于 Node.js 生态的 scp2 和
最近我参与了几个前端手动部署机器的项目,部署流程长期依赖人工操作:从本地打包构建,到登录服务器、定位目录,再到上传压缩包、解压缩完成部署 —— 整套流程步骤繁琐且重复,不仅效率低下,还容易因手动操作出现疏漏。闲暇时,我基于 Node.js 生态的 scp2 和 ssh2 工具,搭建了一套前端项目自动化部署方案。优化后,部署流程被极致简化:开发者只需在本地执行一条 npm run deploy 命令,即可完成从代码构建到服务器部署的全流程自动化。本文将详细分享这套方案的实现思路,带你一步步构建从本地到服务器的前端自动化部署链路,彻底告别重复的手动部署工作。
本文基于一个 React + TypeScript + Vite 的管理系统项目,该项目需要频繁部署到远程服务器。传统的手动部署方式存在以下问题:
手动操作容易出错 #技术分享部署流程繁琐,耗时较长缺乏版本回滚机制部署状态不透明{ "scp2": "^0.5.0", "SSH2": "^1.17.0", "chalk": "^4.1.2"}选择理由:
graph TD A[开始部署] --> B[构建项目] B --> C[处理服务器目录] C --> D[上传 dist 目录] D --> E[部署完成] B --> B1[删除本地 dist] B --> B2[执行 npm run build] B --> B3[验证构建结果] C --> C1[删除 distOld] C --> C2[重命名 dist 为 distOld] D --> D1[建立 SSH 连接] D --> D2[递归上传文件] D --> D3[保持文件权限]class DeployScript { private projectroot: string; private distPath: string; private serverConfig: ServerConfig; build: void; handleServerDirectories: void; uploadDist: void; executeSSHCommand: Promise;}build { console.log('1.开始构建项目...'); const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!this.exists(packageJsonPath)) { throw new Error('❌ 未找到 package.json 文件'); } if (this.exists(this.distPath)) { console.log('删除现有的 dist 目录...'); fs.rmSync(this.distPath, { recursive: true, force: true }); } this.execCommand('npm run build'); if (!this.exists(this.distPath)) { throw new Error('❌ 构建失败,未生成 dist 目录'); } console.log('✅ 项目构建完成');}设计亮点:
async executeSSHCommand(command) { return new Promise((resolve, reject) => { const conn = new Client; conn.on('ready', => { conn.exec(command, (err, stream) => { if (err) { console.error('❌ SSH命令执行失败:', err); reject(err); return; } let stdout = ''; let stderr = ''; stream.on('close', (code, signal) => { conn.end; if (code === 0) { console.log('SSH命令执行成功'); resolve(stdout); } else { console.error('❌ SSH命令执行失败,退出码:', code); reject(new Error(`SSH命令执行失败: ${stderr}`)); } }); stream.on('data', data => { stdout += data.toString; }); stream.stderr.on('data', data => { stderr += data.toString; }); }); }); conn.on('error', err => { console.error('❌ SSH连接失败:', err); reject(err); }); conn.connect(this.serverConfig); });}技术要点:
使用 Promise 包装异步操作,支持 async/await完整的错误处理和状态码检查分离 stdout 和 stderr 输出流async handleServerDirectories { console.log('2.开始处理服务器端目录...'); try { console.log('️ 删除服务器上的 distOld 目录...'); await this.executeSSHCommand( `rm -rf ${this.serverConfig.deployPath}/distOld` ); console.log(' 将现有的 dist 目录重命名为 distOld...'); await this.executeSSHCommand( `if [ -d "${this.serverConfig.deployPath}/dist" ]; then mv ${this.serverConfig.deployPath}/dist ${this.serverConfig.deployPath}/distOld; fi` ); console.log('✅ 服务器端目录处理完成'); } catch (error) { console.error('❌ 服务器端目录处理失败:', error.message); throw error; }}设计优势:
实现版本回滚机制,保留上一版本使用条件判断,避免目录不存在时的错误原子性操作,确保部署过程的一致性uploadDist { console.log('3.开始上传 dist 目录到服务器...'); const server = { host: this.serverConfig.host, port: this.serverConfig.port, username: this.serverConfig.username, password: this.serverConfig.password, path: this.serverConfig.deployPath + '/dist', }; const scpOptions = { ...server, preserve: true, recursive: true, }; scpClient.scp('./dist', scpOptions, err => { if (!err) { console.log(chalk.blue(' RWA系统自动化部署完毕!')); console.log(chalk.green(` dist目录已上传到: ${this.serverConfig.deployPath}/dist`)); } else { console.log(chalk.red('❌ RWA系统自动化部署出现异常'), err); } });}特性说明:
递归上传整个目录结构保持文件权限和时间戳彩色控制台输出,提升用户体验配置管理服务器配置this.serverConfig = { host: 'xx.xx.xx', port: 22, username: 'root', password: 'xxxxx', deployPath: '/data/xxxxx/',};环境变量支持建议将敏感信息移至环境变量:
DEPLOY_HOST=xxxxDEPLOY_PORT=22DEPLOY_USERNAME=rootDEPLOY_PASSWORD=xxxxDEPLOY_PATH=/data/xxxx/总结一键部署 :从构建到部署的全流程自动化版本管理 :支持版本回滚,降低部署风险错误处理 :完善的异常处理和用户提示这套方案不仅提高了部署效率,还大大降低了人为错误的风险。在实际项目中,可以根据具体需求进行定制化改造,比如集成 CI/CD 流程、添加更多环境支持等。
#!/usr/bin/env nodeimport { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import scpClient from 'scp2'; import { Client } from 'ssh2'; import chalk from 'chalk';const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename);class DeployScript { constructor { this.projectRoot = path.resolve(__dirname, '..'); this.distPath = path.join(this.projectRoot, 'dist'); this.outputPath = path.join(this.projectRoot, 'dist.zip');this.serverConfig = { host: 'xxxxx', port: 22, username: 'root', password: 'xxxxx!', deployPath: '/data/xxxx/', }; }execCommand(command, cwd = this.projectRoot) { try { console.log(` 执行命令: ${command}`); execSync(command, { cwd, stdio: 'inherit', encoding: 'utf8', }); console.log(`✅ 命令执行成功: ${command}`); } catch (error) { console.error(`❌ 命令执行失败: ${command}`); console.error(error.message); throw error; } }exists(path) { return fs.existsSync(path); }build { console.log('1.开始构建项目...');const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (!this.exists(packageJsonPath)) { throw new Error('❌ 未找到 package.json 文件'); }if (this.exists(this.distPath)) { console.log('删除现有的 dist 目录...'); fs.rmSync(this.distPath, { recursive: true, force: true }); }this.execCommand('npm run build');if (!this.exists(this.distPath)) { throw new Error('❌ 构建失败,未生成 dist 目录'); }console.log('✅ 项目构建完成'); }async executeSSHCommand(command) { return new Promise((resolve, reject) => { const conn = new Client;conn.on('ready', => { conn.exec(command, (err, stream) => { if (err) { console.error('❌ SSH 命令执行失败:', err); reject(err); return; }let stdout = ''; let stderr = '';stream.on('close', (code, signal) => { conn.end; if (code === 0) { console.log('SSH 命令执行成功'); resolve(stdout); } else { console.error('❌ SSH 命令执行失败,退出码:', code); reject(new Error(`SSH 命令执行失败: ${stderr}`)); } });stream.on('data', data => { stdout += data.toString; });stream.stderr.on('data', data => { stderr += data.toString; }); }); });conn.on('error', err => { console.error('❌ SSH 连接失败:', err); reject(err); });conn.connect({ host: this.serverConfig.host, port: this.serverConfig.port, username: this.serverConfig.username, password: this.serverConfig.password, }); }); }async handleServerDirectories { console.log('2.开始处理服务器端目录...');try { console.log('️ 删除服务器上的 distOld 目录...'); await this.executeSSHCommand( `rm -rf ${this.serverConfig.deployPath}/distOld` );console.log(' 将现有的 dist 目录重命名为 distOld...'); await this.executeSSHCommand( `if [ -d "${this.serverConfig.deployPath}/dist" ]; then mv ${this.serverConfig.deployPath}/dist ${this.serverConfig.deployPath}/distOld; fi` );console.log('✅ 服务器端目录处理完成'); } catch (error) { console.error('❌ 服务器端目录处理失败:', error.message); throw error; } }uploadDist { console.log('3.开始上传 dist 目录到服务器...'); const server = { host: this.serverConfig.host, port: this.serverConfig.port, username: this.serverConfig.username, password: this.serverConfig.password, path: this.serverConfig.deployPath + '/dist', };const scpOptions = { ...server, preserve: true, recursive: true, };scpClient.scp('./dist', scpOptions, err => { if (!err) { console.log(chalk.blue(' RWA 系统自动化部署完毕!')); console.log( chalk.green( ` dist 目录已上传到: ${this.serverConfig.deployPath}/dist` ) ); } else { console.log(chalk.red('❌ RWA 系统自动化部署出现异常'), err); } }); }async run { try { console.log(' 开始一键部署流程...\n'); this.build;await this.handleServerDirectories;this.uploadDist; } catch (error) { console.error('\n❌ 部署失败:', error.message); process.exit(1); } } }const deployScript = new DeployScript; deployScript.run;本文基于实际项目经验总结,如有问题欢迎交流讨论。
来源:墨码行者