node+express&&vue 自制一键部署服务

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.当然,以上还不够简化,最后一步设置开机自启,
 

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

  

  • 21
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
NodeExpressVue是一套非常流行的前后端开发技术栈。在这个技术栈中,Node.js作为后端运行环境,Express作为后端开发框架,而Vue作为前端开发框架。 在这个技术栈中实现评论回复功能可以通过以下几个步骤来完成。 首先,我们需要在后端使用Node.js和Express搭建一个服务器。可以使用Express提供的路由功能来定义接口,用于处理前端发送的请求和返回相应的数据。 其次,前端使用Vue来开发用户界面。Vue提供了许多方便的组件和工具,可以简化开发过程。我们可以使用Vue提供的组件来展示评论列表和评论回复表单。 接下来,我们可以使用Ajax或者Fetch等工具来向后端发送请求。前端发送评论回复的请求时,需要将评论内容和相关信息作为请求的参数发送到后端。 在后端接收到评论回复的请求后,可以进行相应的逻辑处理,比如将评论内容保存到数据库中或者发送邮件通知原评论的作者等。 最后,在前端接收到后端返回的评论回复数据后,可以将回复内容展示在评论列表中,实现实时更新的效果。另外,可以对用户输入的评论内容进行安全过滤,防止XSS攻击等安全问题。 总结来说,使用NodeExpressVue来实现评论回复功能可以分为前后端分离的开发模式,通过前后端交互来实现数据的传输和处理,最终达到用户可实时看到评论回复的效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值