自动部署脚本
前端
JavaScripe
这个文件是一个 **Node.js 部署脚本**,用于将本地 Next.js 项目自动构建、打包并上传到远程服务器。
自动部署脚本
这个文件是一个 Node.js 部署脚本,用于将本地 Next.js 项目自动构建、打包并上传到远程服务器。以下是逐段解析:
步骤解析
bash1. 🧹 删除本地 dist 目录 2. 🔨 执行 pnpm build 3. 📦 打包成 ZIP 4. 🔌 连接 SFTP 5. 🧹 服务器清理旧文件 6. 📤 上传 ZIP 7. 🔧 解压 → 删除 ZIP → 安装依赖 → 重启服务 8. 🧹 删除本地 ZIP
1. 模块导入
| 模块 | 用途 |
|---|---|
ssh2-sftp-client | SFTP 客户端,用于文件上传 |
fs | 文件系统操作(读取、删除、创建文件) |
child_process | 执行本地 shell 命令(如 pnpm build) |
path | 路径处理 |
url | 获取当前文件路径(ESM 替代 __dirname) |
ssh2 | SSH 客户端,用于执行远程命令 |
archiver | 创建 ZIP 压缩包 |
2. 配置区域
jsconst basePath = "/www/wwwroot/***" // 服务器部署目录 const zipName = 'deploy.zip' // 压缩包文件名 const packageTool = 'pnpm' // 包管理工具 const pm2Name = '***' // PM2 进程名
部署配置:
host/port/username/password— 服务器 SSH 连接信息localDirs— 需要打包上传的本地文件/目录preDeployCommands— 上传前在服务器执行的清理命令postDeployCommands— 上传后执行的解压、安装、重启命令
3. createZip 函数
创建 ZIP 压缩包的异步函数:
遍历 localDirs → 检查文件是否存在 → 目录用 archive.directory() / 文件用 archive.file() → 生成 ZIP
压缩级别设为 9(最高压缩率)。
4. execRemoteCommand 函数
通过 SSH 在远程服务器执行命令,特点:
- 支持超时控制(默认 5 分钟)
- 实时输出 stdout/stderr 到控制台
- 非 0 退出码会报错
完整代码示例
jsimport SftpClient from 'ssh2-sftp-client'; import { statSync, existsSync, createWriteStream, unlinkSync, rmSync } from 'fs'; import { execSync } from 'child_process'; import { join, posix, resolve as _resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { Client } from "ssh2"; import archiver from 'archiver'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // ==================== 配置区域 ==================== const basePath = "/www/wwwroot/***" // 服务器目录 const zipName = 'deploy.zip' // ZIP 文件名 const packageTool = 'pnpm' // 包管理工具 const pm2Name = '***' // PM2 进程名 const CONFIG = { // ssh连接配置 host: '***', port: 22, username: '***', password: '***.', remotePath: basePath, // 本地需要上传的目录/文件(相对于项目根目录) // 注意:部署前请先执行 `npm run build` 生成 dist 目录 localDirs: ['dist', 'public', 'package.json', 'next.config.ts', '.env.production'], // 上传前在服务器执行的命令(清理旧文件等) preDeployCommands: [ `rm -rf ${basePath}/dist`, `rm -rf ${basePath}/public`, ], // 上传后在服务器执行的命令(解压、安装依赖、重启等) postDeployCommands: [ `cd ${basePath} && unzip -o ${zipName}}`, `cd ${basePath} && rm -f ${zipName}`, `cd ${basePath} && ${packageTool} install`, `cd ${basePath} && pm2 restart ${pm2Name} || pm2 start npm --name ${pm2Name} -- start`, ], }; // ================================================= const sftp = new SftpClient(); /** * 创建 ZIP 压缩包 */ async function createZip(outputPath, sourcePaths) { return new Promise((resolve, reject) => { const output = createWriteStream(outputPath); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { console.log(`📦 压缩完成: ${outputPath} (${(archive.pointer() / 1024 / 1024).toFixed(2)} MB)\n`); resolve(); }); archive.on('error', (err) => reject(err)); archive.on('warning', (err) => { if (err.code === 'ENOENT') { console.warn(`⚠️ ${err.message}`); } else { reject(err); } }); archive.pipe(output); for (const sourcePath of sourcePaths) { const fullPath = _resolve(__dirname, sourcePath); if (!existsSync(fullPath)) { console.warn(`⚠️ 本地路径不存在,跳过: ${sourcePath}`); continue; } const stat = statSync(fullPath); if (stat.isDirectory()) { archive.directory(fullPath, sourcePath); } else { archive.file(fullPath, { name: sourcePath }); } } archive.finalize(); }); } /** * 执行远程 SSH 命令(带超时) */ async function execRemoteCommand(command, timeoutMs = 300000) { return new Promise((resolve, reject) => { const conn = new Client(); let finished = false; // 超时处理 const timer = setTimeout(() => { if (!finished) { finished = true; conn.end(); reject(new Error(`命令执行超时 (${timeoutMs}ms): ${command}`)); } }, timeoutMs); conn .on('ready', () => { conn.exec(command, (err, stream) => { if (err) { clearTimeout(timer); conn.end(); return reject(err); } let stdout = ''; let stderr = ''; stream .on('close', (code) => { clearTimeout(timer); if (finished) return; finished = true; conn.end(); if (code !== 0) { reject(new Error(`命令退出码 ${code}: ${stderr || stdout}`)); } else { resolve(stdout); } }) .on('data', (data) => { stdout += data; process.stdout.write(data); }) .stderr.on('data', (data) => { stderr += data; process.stderr.write(data); }); }); }) .on('error', (err) => { clearTimeout(timer); if (!finished) { finished = true; reject(err); } }) .connect({ host: CONFIG.host, port: CONFIG.port, username: CONFIG.username, password: CONFIG.password, readyTimeout: 20000, }); }); } /** * 主部署流程 */ async function deploy() { console.log('🚀 开始部署...'); console.log(`📡 目标服务器: ${CONFIG.host}`); console.log(`📁 远程目录: ${CONFIG.remotePath}\n`); const zipPath = join(__dirname, zipName); try { // 1. 清理本地 dist 目录 const localDistPath = join(__dirname, 'dist'); if (existsSync(localDistPath)) { console.log('🧹 清理本地 dist 目录...'); rmSync(localDistPath, { recursive: true, force: true }); console.log('✅ 本地 dist 已清理\n'); } // 2. 执行 pnpm build console.log('🔨 执行 pnpm build...'); execSync('pnpm build', { cwd: __dirname, stdio: 'inherit' }); console.log('✅ 构建完成\n'); // 3. 创建 ZIP 压缩包 console.log('📦 正在打包文件...'); await createZip(zipPath, CONFIG.localDirs); // 4. 连接 SFTP console.log('🔌 连接 SFTP...'); await sftp.connect({ host: CONFIG.host, port: CONFIG.port, username: CONFIG.username, password: CONFIG.password, }); console.log('✅ SFTP 连接成功\n'); // 5. 执行部署前命令 if (CONFIG.preDeployCommands.length > 0) { console.log('🧹 执行部署前清理...'); for (const cmd of CONFIG.preDeployCommands) { console.log(`> ${cmd}`); await execRemoteCommand(cmd); } console.log(''); } // 6. 上传 ZIP 文件 console.log('📤 上传压缩包...'); const remoteZipPath = posix.join(CONFIG.remotePath, zipName); await sftp.put(zipPath, remoteZipPath); console.log(`✅ 上传完成: ${remoteZipPath}\n`); // 7. 执行部署后命令(解压、安装、重启) if (CONFIG.postDeployCommands.length > 0) { console.log('🔧 执行部署后命令...'); for (const cmd of CONFIG.postDeployCommands) { console.log(`> ${cmd}`); await execRemoteCommand(cmd, 600000); // 10分钟超时 } console.log(''); } console.log('🎉 部署完成!'); } catch (err) { console.error('\n❌ 部署失败:', err.message); process.exit(1); } finally { // 8. 清理本地临时 ZIP 文件 try { if (existsSync(zipPath)) { unlinkSync(zipPath); } // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore cleanup error } await sftp.end(); } } // 运行部署 deploy();