Node.js 实现前端自动化部署:服务器无复杂依赖实战(含源码)
在前端开发中,手动部署项目(打包 → 上传服务器 → 解压替换)重复且低效,尤其在多环境测试、频繁迭代场景下,自动化部署能大幅节省时间。本文基于 Node.js 开发轻量自动化部署脚本,无需 Jenkins、GitLab CI 等复杂工具,仅通过 ssh2(远程连接)、archiver(压缩打包)等基础依赖,实现 “本地打包 → 远程上传 → 服务器解压部署” 全流程自动化,适配 Vue、React、Vite 等各类前端项目。
一、核心原理与环境准备
1. 自动化部署核心流程
2. 环境说明
- 本地环境:Node.js 14.x+(需安装 npm/yarn);
- 服务器环境:Linux(CentOS/Ubuntu,支持 SSH 连接);
- 依赖库:
-
- ssh2:Node.js 实现 SSH 远程连接与命令执行;
-
- archiver:压缩前端打包产物为 zip 包;
-
- dotenv:管理服务器配置(IP、账号、密码等敏感信息);
-
- ora:终端加载动画,提升交互体验。
3. 环境准备步骤
(1)本地项目初始化(若已有项目可跳过)
# 新建项目(示例)
mkdir frontend-auto-deploy && cd frontend-auto-deploy
npm init -y
(2)安装依赖库
# 安装核心依赖
npm install ssh2 archiver dotenv ora --save-dev
(3)服务器配置准备
- 确保服务器开启 SSH 服务(默认 22 端口);
- 拥有服务器账号密码(或密钥),且账号具备目标目录的读写权限;
- 提前创建项目部署目录(如 /usr/local/nginx/html/frontend)。
二、核心源码实现(4 个文件搞定)
1. 配置文件(.env):管理敏感信息
在项目根目录新建 .env 文件,存储服务器连接配置与项目路径,避免硬编码:
# .env 文件(需添加到 .gitignore,避免泄露)
# 服务器 SSH 配置
SERVER_HOST=192.168.1.100 # 服务器 IP
SERVER_PORT=22 # SSH 端口(默认 22)
SERVER_USER=root # 服务器登录账号
SERVER_PASSWORD=123456 # 服务器登录密码(密钥登录见下文扩展)
# 项目配置
LOCAL_BUILD_DIR=./dist # 本地打包产物目录(Vue/React 默认为 dist,Vite 默认为 dist)
LOCAL_ZIP_NAME=frontend-deploy.zip # 本地压缩包名称
SERVER_TEMP_DIR=/tmp/ # 服务器临时上传目录(推荐 /tmp/,无需额外权限)
SERVER_DEPLOY_DIR=/usr/local/nginx/html/frontend # 服务器部署目录(需提前创建)
2. 工具函数(utils.js):封装通用功能
在项目根目录新建 utils.js,封装压缩、日志输出等通用工具函数:
// utils.js
const archiver = require('archiver');
const fs = require('fs');
const path = require('path');
const ora = require('ora');
/**
* 压缩本地打包产物为 zip 包
* @param {string} sourceDir - 本地打包目录(如 ./dist)
* @param {string} zipPath - 压缩包输出路径(如 ./frontend-deploy.zip)
* @returns {Promise
*/
exports.zipLocalDir = async (sourceDir, zipPath) => {
const spinner = ora('正在压缩本地打包产物...').start();
return new Promise((resolve, reject) => {
// 创建输出流
const output = fs.createWriteStream(zipPath);
// 初始化压缩实例(zip 格式,压缩级别 9)
const archive = archiver('zip', { zlib: { level: 9 } });
// 监听错误
output.on('error', (err) => {
spinner.fail('压缩失败!');
reject(err);
});
archive.on('error', (err) => {
spinner.fail('压缩失败!');
reject(err);
});
// 完成压缩
archive.pipe(output);
archive.directory(sourceDir, false); // 递归添加目录下所有文件(不含父目录)
archive.finalize();
output.on('close', () => {
spinner.succeed(`压缩成功!压缩包大小:${(archive.pointer() / 1024 / 1024).toFixed(2)}MB`);
resolve();
});
});
};
/**
* 执行本地命令(如 npm run build)
* @param {string} cmd - 命令字符串
* @param {string} desc - 命令描述(用于日志)
* @returns {Promise
*/
exports.execLocalCmd = async (cmd, desc) => {
const { exec } = require('child_process');
const spinner = ora(`正在${desc}...`).start();
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => {
if (error) {
spinner.fail(`${desc}失败!`);
console.error('错误信息:', stderr);
reject(error);
return;
}
spinner.succeed(`${desc}成功!`);
resolve();
});
});
};
/**
* 检查本地目录是否存在
* @param {string} dirPath - 目录路径
* @returns {boolean}
*/
exports.checkLocalDirExist = (dirPath) => {
const resolvedPath = path.resolve(dirPath);
if (!fs.existsSync(resolvedPath)) {
console.error(`错误:本地目录 ${resolvedPath} 不存在,请先打包项目!`);
process.exit(1);
}
return true;
};
3. 服务器操作(server.js):SSH 连接与远程命令
新建 server.js,封装 SSH 连接、文件上传、远程命令执行等服务器操作:
// server.js
const { Client } = require('ssh2');
const fs = require('fs');
const path = require('path');
const ora = require('ora');
/**
* SSH 连接服务器
* @param {Object} config - 服务器配置(host, port, username, password)
* @returns {Promise
*/
const connectServer = async (config) => {
const spinner = ora('正在连接服务器...').start();
return new Promise((resolve, reject) => {
const conn = new Client();
conn.on('ready', () => {
spinner.succeed('服务器连接成功!');
resolve(conn);
}).on('error', (err) => {
spinner.fail('服务器连接失败!');
console.error('错误信息:', err.message);
reject(err);
}).connect(config);
});
};
/**
* 上传本地文件到服务器
* @param {Client} conn - SSH 客户端实例
* @param {string} localFilePath - 本地文件路径
* @param {string} serverFilePath - 服务器文件路径
* @returns {Promise
*/
exports.uploadFileToServer = async (conn, localFilePath, serverFilePath) => {
const spinner = ora('正在上传文件到服务器...').start();
return new Promise((resolve, reject) => {
conn.sftp((err, sftp) => {
if (err) {
spinner.fail('SFTP 连接失败!');
reject(err);
return;
}
// 上传文件
const readStream = fs.createReadStream(localFilePath);
const writeStream = sftp.createWriteStream(serverFilePath);
writeStream.on('close', () => {
spinner.succeed('文件上传成功!');
resolve();
}).on('error', (err) => {
spinner.fail('文件上传失败!');
reject(err);
});
readStream.pipe(writeStream);
});
});
};
/**
* 执行服务器远程命令
* @param {Client} conn - SSH 客户端实例
* @param {string} cmd - 远程命令
* @param {string} desc - 命令描述(用于日志)
* @returns {Promise
*/
exports.execServerCmd = async (conn, cmd, desc) => {
const spinner = ora(`正在${desc}...`).start();
return new Promise((resolve, reject) => {
conn.exec(cmd, (err, stream) => {
if (err) {
spinner.fail(`${desc}失败!`);
reject(err);
return;
}
// 捕获命令输出(便于调试)
let output = '';
stream.on('data', (data) => {
output += data.toString();
}).on('close', (code) => {
if (code === 0) {
spinner.succeed(`${desc}成功!`);
resolve();
} else {
spinner.fail(`${desc}失败!`);
console.error('命令输出:', output);
reject(new Error(`命令执行失败,退出码:${code}`));
}
});
});
});
};
/**
* 初始化服务器操作(连接 + 上传 + 部署 + 断开)
* @param {Object} config - 服务器配置
* @param {string} localZipPath - 本地压缩包路径
* @param {string} serverTempZipPath - 服务器临时压缩包路径
* @param {string} serverDeployDir - 服务器部署目录
* @returns {Promise
*/
exports.deployToServer = async (config, localZipPath, serverTempZipPath, serverDeployDir) => {
let conn = null;
try {
// 1. 连接服务器
conn = await connectServer(config);
// 2. 上传压缩包到服务器临时目录
await this.uploadFileToServer(conn, localZipPath, serverTempZipPath);
// 3. 服务器解压压缩包到部署目录(先清空旧文件,避免缓存)
const deployCmd = [
`rm -rf ${serverDeployDir}/*`, // 清空部署目录(谨慎使用!确保路径正确)
`unzip -o ${serverTempZipPath} -d ${serverDeployDir}`, // 覆盖解压
`rm -rf ${serverTempZipPath}` // 删除临时压缩包
].join(' && ');
await this.execServerCmd(conn, deployCmd, '部署项目');
} finally {
// 4. 断开服务器连接
if (conn && conn._sock && conn._sock.readable) {
conn.end();
console.log('
服务器连接已断开,部署流程全部完成!');
}
}
};

4. 主脚本(deploy.js):串联全流程
新建 deploy.js,作为部署入口脚本,串联 “本地打包 → 压缩 → 服务器部署” 全流程:
// deploy.js
require('dotenv').config(); // 加载 .env 配置
const path = require('path');
const { zipLocalDir, execLocalCmd, checkLocalDirExist } = require('./utils');
const { deployToServer } = require('./server');
// 读取配置(从 .env 文件)
const config = {
server: {
host: process.env.SERVER_HOST,
port: process.env.SERVER_PORT || 22,
username: process.env.SERVER_USER,
password: process.env.SERVER_PASSWORD
},
local: {
buildDir: process.env.LOCAL_BUILD_DIR || './dist',
zipName: process.env.LOCAL_ZIP_NAME || 'frontend-deploy.zip',
zipPath: path.resolve(__dirname, process.env.LOCAL_ZIP_NAME || 'frontend-deploy.zip')
},
serverPath: {
tempZipPath: path.join(process.env.SERVER_TEMP_DIR || '/tmp/', process.env.LOCAL_ZIP_NAME || 'frontend-deploy.zip'),
deployDir: process.env.SERVER_DEPLOY_DIR || '/usr/local/nginx/html/frontend'
}
};
/**
* 主部署流程
*/
const main = async () => {
try {
console.log('==================== 开始前端自动化部署 ====================');
// 1. 本地打包项目(根据项目打包命令调整,如 npm run build:prod)
await execLocalCmd('npm run build', '打包前端项目');
// 2. 检查打包目录是否存在
checkLocalDirExist(config.local.buildDir);
// 3. 压缩打包产物
await zipLocalDir(config.local.buildDir, config.local.zipPath);
// 4. 部署到服务器
await deployToServer(
config.server,
config.local.zipPath,
config.serverPath.tempZipPath,
config.serverPath.deployDir
);
console.log('
==================== 部署成功! ====================');
console.log(`✅ 本地打包目录:${config.local.buildDir}`);
console.log(`✅ 服务器部署目录:${config.serverPath.deployDir}`);
console.log(`✅ 访问地址:http://${config.server.host}(需配置服务器 Nginx)`);
} catch (error) {
console.error('
==================== 部署失败! ====================');
console.error('错误信息:', error.message);
process.exit(1); // 部署失败退出进程
}
};
// 执行部署
main();
三、使用说明与部署验证
1. 脚本配置调整(关键!)
根据自身项目情况修改 .env 文件:
- SERVER_HOST:替换为你的服务器公网 / 内网 IP;
- SERVER_USER/SERVER_PASSWORD:替换为服务器登录账号密码;
- LOCAL_BUILD_DIR:前端项目打包产物目录(Vue CLI 为 ./dist,Vite 为 ./dist,React 为 ./build);
- SERVER_DEPLOY_DIR:服务器上 Nginx 或 Apache 指向的静态文件目录(如 /usr/local/nginx/html/frontend)。
2. 执行部署命令
在项目根目录打开终端,执行以下命令启动部署:
# 执行部署脚本
node deploy.js
3. 部署成功日志示例
==================== 开始前端自动化部署 ====================
√ 正在打包前端项目...
√ 正在压缩本地打包产物...
√ 压缩成功!压缩包大小:1.23MB
√ 正在连接服务器...
√ 服务器连接成功!
√ 正在上传文件到服务器...
√ 文件上传成功!
√ 正在部署项目...
√ 部署成功!
==================== 部署成功! ====================
✅ 本地打包目录:./dist
✅ 服务器部署目录:/usr/local/nginx/html/frontend
✅ 访问地址:http://192.168.1.100
4. 服务器 Nginx 配置(可选)
若需通过域名 / IP 访问部署的项目,需配置 Nginx 静态资源指向:
# /etc/nginx/conf.d/frontend.conf(新建配置文件)
server {
listen 80;
server_name 192.168.1.100; # 服务器 IP 或域名
location / {
root /usr/local/nginx/html/frontend; # 与 SERVER_DEPLOY_DIR 一致
index index.html index.htm;
try_files $uri $uri/ /index.html; # 解决 Vue/React 路由刷新 404 问题
}
# 静态资源缓存配置(可选)
location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
root /usr/local/nginx/html/frontend;
expires 7d; # 缓存 7 天
}
}
重启 Nginx 生效:
nginx -t # 验证配置
nginx -s reload # 重启 Nginx
四、扩展功能与避坑指南
1. 扩展功能:密钥登录(替代密码登录,更安全)
若服务器禁用密码登录,需通过 SSH 密钥认证,修改 .env 和 server.js:
# .env 新增密钥配置(替换密码登录)
SERVER_PRIVATE_KEY_PATH=~/.ssh/id_rsa # 本地私钥路径(如 Windows 为 C








