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