node+expressvue 自制一键部署服务
2026/8/7 22:44:49 网站建设 项目流程
tips:仅针对nuxt项目部署,可自行调整

用到的npm包如下

express //用于创建服务器和路由。 ssh2 //用于通过 SSH 连接远程服务器。 child_process //用于执行命令行命令。 compressing //用于压缩文件。 fs //用于文件系统操作。 path //用于处理和转换文件路径。 archiver //用于创建压缩文件。 sqlite //用于数据库操作。 WebSocket //用于 WebSocket 通信。

1.搭建node+express,此步骤省略,我们直接进入主题,

2.创建ws.js文件 搭建webSocket服务,将部署过程中的内容实时发送给前端页面

const WebSocket = require('ws'); const url = require('url'); const clientslist = {}; const wss = new WebSocket.Server({ port:5001 }); wss.on('connection', function connection(ws,req) { let clientsId=url.parse(req.url, true).query.id clientslist[clientsId]=ws clientslist[clientsId].on('message', function incoming(message,reqs) { let arr=JSON.parse((message).toString()) if(!arr.clientsId){ arr.clientsId=clientsId clientslist.serves.send(JSON.stringify(arr)) }else{ let msg={ message:arr.message } clientslist[arr.clientsId].send(JSON.stringify(msg)) } }); ws.on('close', function(err) { console.log(err,'错误') console.log('The client has disconnected.'); }); ws.on('error', function(error) { console.error('WebSocket error:', error); }); });

3.新建一个文件夹sqlite ,在该文件夹下创建两个文件。

第一个dataBase.db(当然也可以用JSON文件代替,若用JSON文件,数据库的相关操作就不需要了,要换成对JSON数据的操作),存放部署所需要的一些具体参数,比如服务器相关数据和打包的一些命令

第二个文件是db.js 是一个连接数据库的方法,代码如下:

const sqlite3 = require('sqlite3').verbose(); var path = require('path'); const dbPath = path.resolve(__dirname, 'database.db'); const db = new sqlite3.Database(dbPath, (err) => { if (err) { console.error('数据库连接失败:', err.message); } else { console.log('成功连接到数据库'); } }); module.exports = db;

4.新建一个automatic.js文件,该文件内包含部署的整个流程:

流程包括

  • 更新状态为开始。
  • 获取配置数据。
  • 打包项目。
  • 压缩文件。
  • 连接远程服务器。
  • 上传压缩包。
  • 解压文件。
  • 安装依赖。
  • 启动项目。
  • 删除本地和远程的压缩文件。
  • 更新状态为完成。

从sqlite获取部署所需要数据
let getauticdata = (val) => { return new Promise((resolve, reject) => { db.all(`SELECT * FROM main.pipeline WHERE id =?`, [val], (err, rows) => { resolve(rows[0]); }); }); }; //获取到的数据包含 部署对应的服务器的ip、端口、用户名、密码,本地项目所在目录,打包命令
打包本地项目
let buildpack = (config, callback) => { return new Promise((resolve, reject) => { const commands = config.PackagingCommand;//命令 const child = exec(commands, { cwd: config.basefilepath, encoding: 'utf-8', stdio: 'inherit' });//执行命令 child.stdout.on('data', (data) => { callback(data.toString()); console.log(`标准输出: ${data}`); //执行过程输出内容 }); child.on('close', (code) => { if (code == 0) { //code为 0 执行成功 resolve(); } else { reject(`退出码${code}`); } }); child.on('error', (error) => { reject(`执行错误: ${error}`); }); }); };
压缩上一步打包完成的产物
let filesZip = (config) => { return new Promise((resolve, reject) => { const output = fs.createWriteStream(path.join(config.basefilepath, 'build.zip')); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', () => { resolve(); }); archive.pipe(output); const filesAndDirs = JSON.parse(config.packagingDirectory); filesAndDirs.forEach(item => { const stats = fs.statSync(path.join(config.basefilepath, item)); if (stats.isDirectory()) { archive.directory(path.join(config.basefilepath, item), path.basename(item)); } else { archive.file(path.join(config.basefilepath, item), { name: path.basename(item) }); } }); archive.finalize(); }); };
使用ssh2 连接远程服务器
let conntossh = (config) => { return new Promise((resolve, reject) => { conn.on('ready', () => { resolve(); }).connect({ host: config.serveHost,//服务器ip port: config.servePort,//端口 username: config.serveUserName,//用户名 password: config.servePsd//密码 }); }); };
使用sftp将压缩打包产物 传输远程服务器
let conntoftp = (config) => { return new Promise((success, errors) => { conn.sftp((err, sftp) => { sftp.fastPut(path.join(config.basefilepath, 'build.zip'), config.remoteDirpath + 'build.zip', {}, (err) => { if (err) { errors(err); } else { sftp.end(); success(); } }); }); }); }; //config.basefilepath 本地项目目录 //config.remoteDirpath 远程服务器指定目录
在远程服务器上解压 ZIP 文件。
let unzip = (config, callback) => { return new Promise((success, errors) => { const command = 'unzip -o build.zip'; //解压并强制覆盖命令 const remoteDirectory = config.remoteDirpath; //指定目录 conn.exec(`cd ${remoteDirectory} && ${command}`, (err, stream) => { if (err) { errors(err); } else { stream.on('close', (code, signal) => { success(); }).on('data', (data) => { callback(data.toString()); console.log('STDOUT: ' + data); }); } }); }); };
在服务器上安装依赖(因为nuxt项目是服务端渲染的,和普通vue项目不一样,需要在服务端启动一个端口,所以需要安装依赖)
let installreliance = (config, callback) => { return new Promise((resolve, reject) => { const command = 'npm i'; conn.exec(`cd ${config.remoteDirpath} && ${command}`, (err, stream) => { if (err) { reject(err); } else { stream.on('close', (code, signal) => { resolve(); }).on('data', (data) => { callback(data.toString()); console.log('STDOUT: ' + data); }).stderr.on('data', (data) => { callback(data.toString()); console.error('STDERR: ' + data.toString()); }); } }); }); };
使用PM2,启动项目,可保证项目持久化,
let startrun = (config, callback) => { return new Promise((success, errors) => { const command = 'pm2 start ecosystem.config.js'; const remoteDirectory = config.remoteDirpath; conn.exec(`cd ${remoteDirectory} && ${command}`, (err, stream) => { if (err) { errors(err); } else { stream.on('close', (code, signal) => { success(); }).on('data', (data) => { callback(data.toString()); console.log('STDOUT: ' + data); }); } }); }); };
删除本地和服务器上的压缩文件
//删除本地压缩文件 exec('del build.zip', { cwd: config.basefilepath, encoding: 'utf-8', stdio: 'inherit' }); //删除服务器上的压缩文件 const command = 'rm -f build.zip'; const remoteDirectory = config.remoteDirpath; conn.exec(`cd ${remoteDirectory} && ${command}`
更新数据库中的部署状态
let updatastatus = (id, runStates) => { return new Promise((resolve, reject) => { db.all(`UPDATE main.pipeline SET runStates=? WHERE id =?`, [runStates, id], (err, rows) => { resolve(); }); }); };
WebSocket 连接
const ws = new WebSocket('ws://localhost:5001?id=serves'); ws.on('message', function incoming(message) { let arr = JSON.parse((message).toString()); if (arr.start) { console.log(arr.id); activateDeployment(arr.id, arr.clientsId); } });

通过 WebSocket 连接到服务器并监听消息。当收到消息并解析为 JSON 对象后,如果包含 start 字段,则调用 activateDeployment 方法 开始部署

主要部署方法
let activateDeployment = async (id, clientsId) => { let arr = { clientsId: clientsId }; try { await updatastatus(id, 1); arr.message = 'start'; ws.send(JSON.stringify(arr)); let config = await getauticdata(id); arr.message = '正在准备项目打包......'; ws.send(JSON.stringify(arr)); await buildpack(config, function (val) { arr.message = val; ws.send(JSON.stringify(arr)) }); arr.message = `打包完成`; ws.send(JSON.stringify(arr)); arr.message = `正在压缩文件`; ws.send(JSON.stringify(arr)); await filesZip(config); arr.message = `压缩文件完成`; ws.send(JSON.stringify(arr)); arr.message = `正在连接服务器`; ws.send(JSON.stringify(arr)); await conntossh(config); arr.message = `服务器连接成功`; ws.send(JSON.stringify(arr)); arr.message = `正在上传压缩包`; ws.send(JSON.stringify(arr)); await conntoftp(config); arr.message = `上传完成`; ws.send(JSON.stringify(arr)); arr.message = `正在解压`; ws.send(JSON.stringify(arr)); await unzip(config, function (val) { arr.message = val; ws.send(JSON.stringify(arr)) }); arr.message = `解压完成`; ws.send(JSON.stringify(arr)); arr.message = `准备安装依赖`; ws.send(JSON.stringify(arr)); await installreliance(config, function (val) { arr.message = val; ws.send(JSON.stringify(arr)) }); arr.message = `依赖安装完成`; ws.send(JSON.stringify(arr)); arr.message = `正在准备启动项目`; ws.send(JSON.stringify(arr)); await startrun(config, function (val) { arr.message = val; ws.send(JSON.stringify(arr)) }); arr.message = `项目启动成功`; ws.send(JSON.stringify(arr)); await deletezipfile(config); await deleteremotezipfile(config); await updatastatus(id, 2); arr.message = `end`; ws.send(JSON.stringify(arr)); } catch (error) { await updatastatus(id, -1); arr.message = error; ws.send(JSON.stringify(arr)); arr.message = `end`; ws.send(JSON.stringify(arr)); } };

开始部署将数据库状态更新为1 代表部署中,部署中的每一步都会将输出内容通过ws发送到前端

中间出错出错的话会将数据状态跟新为-1,代表部署失败,并将错误信息发送给前端,若一路畅通,将状态跟新为2,代表部署成功

5.前端调用

runs(id){ this.log[id]='' let ws=new WebSocket('ws://localhost:5001?id='+id) ws.onopen = () => { console.log('WebSocket连接已打开'); let arr={"start":true,"id":id} ws.send(JSON.stringify(arr)); }; ws.onmessage = (event) => { let arr=JSON.parse(event.data); console.log(arr.message) this.log[id]+=(arr.message+'<br>') if(this.logdialog){ var box = this.$refs.logcont; // 使用scrollTop将滚动条滚动到最低 box.scrollTop = box.scrollHeight; } if(arr.message=='start'){ this.getpagedata() } if(arr.message=='end'){ console.log('部署完成') ws.close() this.getpagedata() } }; ws.onclose = () => { console.log('WebSocket连接已关闭'); // 可以尝试重新连接 // this.connect(); }; },

到这里,整个部署流程已经可以结束了,不过。。。。

不过嘛,每次都得启动两个项目,一个是服务端,一个是vue前端项目,但是我们做的是一键部署为的就是便捷,如果中间这两个项目因为某些原因被中断掉,还要重新去启动,这样就不够便捷了所以就有了以下的操作

6.配置全局启命令

1.创建文件夹,放在D盘的某个目录下,在该文件夹下执行 npm init 命令 生成 pakejosn 文件并修改如下

{ "name": "runpipeline", "version": "1.0.0", "description": "", "bin": { "runpipeline": "runpipeline.js" }, "author": "", "license": "ISC" } // bin 中 runpipeline 为自定义的全局命令 runpipeline.js 是全局命令执行的是该文件 下面会讲到

2.将 上面搭建的node项目和vue项目全部都放到当前文件 ,在vue项目的主目录下 新建一个ecosystem.config.js内容如下

module.exports = { apps: [ { name: 'pipeLineUi', //服务名称---自定义 script: 'npm', args: 'run serve',//命令 watch: true, //开启文件发生变化自动重启 } ] }

3.新建runpipeline.js文件 代码如下

#!/usr/bin/env node const { exec} = require('child_process'); (async()=>{ let serverstatus= await checkServestatus('www') //检查服务端的是否已经启动 if(serverstatus==0){ // 若没启动 先启动部署服务 在启动客户端 await startserve() startclient() }else{ startclient // 若启动 直接启动客户端 } }) let startclient=async ()=>{ let clientstatus= await checkServestatus('pipeLineUi') //检查客户端是否启动 if(clientstatus==0){ // 若没启动 启动客户端 再打开浏览器 await startclients() exec('start chrome http://www.example.com'); //打开浏览器 }else{ exec('start chrome http://www.example.com'); // 若启动 直接打开浏览器 } } let startserve=()=>{ return new Promise((resolve, reject) => { const child = exec('pm2 start ./serve/bin/www',{cwd:__dirname, encoding: 'utf-8',stdio: 'inherit',timeout: 100000,shell: true }); child.stdout.on('data', (data) => { console.log(`${data}`); }) child.on('close', (code) => { if(code==0){ console.log('部署服务启动成功') resolve() }else{ console.log('部署服务启动失败') console.log(`退出码${code}`) } }); child.on('error', (error) => { console.log(`错误码?${code}`) }); }) } let startclients=()=>{ return new Promise((resolve, reject) => { const child = exec('pm2 start ./client/ecosystem.config.js',{cwd:__dirname, encoding: 'utf-8',stdio: 'inherit',timeout: 100000,shell: true }); child.stdout.on('data', (data) => { console.log(`${data}`); }) child.on('close', (code) => { if(code==0){ console.log('客户端启动成功') resolve() }else{ console.log('客户端启动失败') console.log(`退出码${code}`) } }); child.on('error', (error) => { console.log(`错误码?${code}`) }); }) }

两个项目中全都使用 pm2 启动,可保证持久化

下一步 在当前目录的终端中执行 npm link 之后就可以在任意目录下直接输入 runpipeline 就可以启动一键部署了

7.当然,以上还不够简化,最后一步设置开机自启,

嗯.............................................................................................................................................................................................................................还没实现。。。。。。。。。后面补上

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询