• 一三六、从零到一实现自动化部署


    上一篇讲到服务器部署Node项目、Vue spa静态项目、ssr项目

    然而每次都要去手动部署,不仅麻烦,对Nginx,Linux不熟悉的也不友好,目前的常见的自动化部署有jenkinsdocker等,但是有一定的学习成本,本文通过Node+vue的实现一键自动化部署。

    技术栈

    前端:

    1. vue (MVVM 框架)
    2. element-ui (ui框架)
    3. axios (接口请求)
    4. socket.io-client (创建 io 实例)
    5. vue-socket.io ($socket挂载到vue实例)

    服务端:

    1. Node.js
    2. koa
    3. socket.io
    4. log4js
    5. pm2

    实现思路

    前端选择要部署的项目点击部署按钮,调用node提供的部署接口,node拿到参数执行部署脚本,并将部署 log 通过websocket返回给前端

    流程图如下
    image1.png

    具体实现

    服务端

    1、开启socket服务
    // app.js
    // 开启 socket 服务
    // socket模块
    const {Server} = require('socket.io');
    // 为socket新起个端口
    const io = new Server(9001, {
        // 是否启用与 Socket.IO v2 客户端的兼容性。
        allowEIO3: true,
        transports: ['websocket', 'polling'],
        cors: {
            origin: '*',
            methods: ['GET', 'POST']
        }
    });
    
    
    io.on('connection', socket => {
        console.log('connection socket连接成功');
        app.context.socketIo = socket;
    });
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    2. 实现deploy接口
    // app/api/v1/deploy.js
    const Router = require('koa-router');
    
    const {DeployValidator} = require('@validators/deploy');
    const {Deploy} = require('@deploy/index.js');
    const {Resolve} = require('@lib/helper');
    const res = new Resolve();
    
    const router = new Router({
        prefix: '/api/v1'
    });
    
    //
    router.post('/deploy', async ctx => {
        // 通过验证器校验参数是否通过
        const v = await new DeployValidator().validate(ctx);
    
        // 搜索写真
        const [err, data] = await Deploy.runSh({
            kw: v.get('body.kw'),
            socketIo: ctx.socketIo
        });
        console.log('🚀 > data', err, data);
    
        if (!err) {
            // 返回结果
            ctx.response.status = 200;
            ctx.body = res.json(data);
        } else {
            ctx.body = res.fail(err);
        }
    });
    
    module.exports = router;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    3. 判断部署脚本是否存在
    // app/deploy/index.js
    const path = require('path');
    const fs = require('fs');
    const runCmd = require('../lib/runCmd.js');
    const logger = require('../lib/logger.js');
    class Deploy {
        // 执行部署脚本
        // koa 注意异步 404 的问题
        static runSh(params) {
            const {kw, socketIo} = params;
            let shPath = path.join(__dirname, `../../sh/${kw}.sh`);
            return new Promise(resolve => {
                try {
                    fs.access(shPath, fs.constants.F_OK, err => {
                        if (err) {
                            return resolve([`${shPath}该脚本不存在`, null]);
                        } else {
                            runCmd(
                                'sh',
                                [shPath],
                                text => {
                                    resolve([null, text]);
                                },
                                socketIo,
                                kw
                            );
                        }
                    });
                } catch (e) {
                    logger.info(e);
                    resolve([e, null]);
                }
            });
        }
    
    }
    
    module.exports = {
        Deploy
    };
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    4. 执行脚本并输出log
    // app/lib/runCmd.js
    const logger = require('./logger');
    
    // 使用子进程执行命令
    function runCmd(cmd, args, callback, socketIo, kw) {
        const spawn = require('child_process').spawn;
        const child = spawn(cmd, args);
        let resp = '当前执行路径:' + process.cwd() + '\n';
        logger.info(resp);
        socketIo && socketIo.emit(kw || 'message', resp);
        child.stdout.on('data', buffer => {
            callback('开始部署');
            let info = buffer.toString();
            info = `${new Date().toLocaleString()}: ${info}`;
            resp += info;
            logger.info(info);
            socketIo && socketIo.emit(kw || 'message', info);
        // log 较多时,怎么实时将消息通过接口返给前端,只能是 socket
        });
        child.stdout.on('end', () => {
            callback(resp);
        });
    
        // shell 脚本执行错误信息也返回
        // let errorMsg = "";
        // 错误信息 end、正常信息 end 可能有先后,统一成一个信息
        child.stderr.on('data', buffer => {
            let info = buffer.toString();
            info = `${new Date().toLocaleString()}: ${info}`;
            resp += info;
            logger.info(info);
            socketIo && socketIo.emit(kw || 'message', info);
        });
        child.stderr.on('end', () => {
            callback(resp);
        });
    }
    
    module.exports = runCmd;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    // app/lib/logger.js
    const log4js = require('log4js');
    const logger = log4js.getLogger();
    logger.level = 'debug';
    
    module.exports = logger;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    客户端(前端)

    1、链接socketio
    const wsPath =process.env.NODE_ENV === 'production'? 'ws://119.91.139.245:9001': 'ws://localhost:9001';
    
    import SocketIO from 'socket.io-client';
    
    import VueSocketIO from 'vue-socket.io';
    
    const options = {
        autoConnect: false
    };
    Vue.use(new VueSocketIO({
        // nodejs-koa-blog/admin-blog/src/main.js
        // 调试模式,开启后将在命令台输出蓝色的相关信息
        debug: true,
        connection: SocketIO(wsPath, options)
    }));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    2、页面UI + 逻辑
    // nodejs-koa-blog/admin-blog/src/views/deploy/index.vue
    <template>
        <div class="category">
            <div class="search">
                <el-form
                    ref="deployForm"
                    :model="deployForm"
                    inline>
                    <el-form-item label="选择要部署的服务:" prop="status">
                        <el-select
                            v-model="deployForm.deploy"
                            placeholder="请选择状态"
                            clearable>
                            <el-option v-for="opt in deployOpt" :key="opt" :label="opt" :value="opt" />
                        </el-select>
                    </el-form-item>
    
                    <el-form-item>
                        <el-button type="primary" size="medium" @click="start">
                            部署
                        </el-button>
                    </el-form-item>
                </el-form>
            </div>
            <div class="log-content">
                <el-card v-for="card,index in Object.keys(timelineData)" :key="index" class="box-card">
                    <template #header>
                        <div class="clearfix">
                            <span>{{ card }}</span>
                        </div>
                    </template>
                    <el-timeline :reverse="true">
                        <el-timeline-item
                            v-for="activity,idx in timelineData[card]"
                            :key="idx"
                            :timestamp="activity.timestamp">
                            {{ activity.content }}
                        </el-timeline-item>
                    </el-timeline>
                </el-card>
            </div>
        </div>
    </template>
    <script>
    import {deploy} from '@/api/deploy';
    export default {
        name: 'DeployIndex',
        data() {
            return {
                id: '',
                uid: 2,
                isSocket: false,
                msg: [],
                deployOpt: ['admin', 'servers', 'frontend', 'install', 'demo'],
                deployForm: {
                    deploy: 'admin'
                },
                timelineData: {}
            };
        },
        beforeUnmount() {
            this.isSocket = false;
            this.$socket.disconnect();
        },
        mounted() {
            if (!this.isSocket){
                // 连接socket服务
                this.$socket.connect();
                // 触发server端的start事件
                this.$socket.emit('start', this.uid);
            }
        },
        sockets: {
            connect() {
                this.id = this.$socket.id;
                console.log('connect---监听socket连接状态', this.id);
            },
            disconnect(reason) {
                console.log('disconnect--socket断开服务的原因:', reason);
            },
            message(data) {
                // 监听message事件,方法是后台定义和提供的
                console.log('message 接收到服务端传回的参数:', data );
            },
            demo(data) {
                this.setSocketData('demo', data);
            },
            admin(data) {
                this.setSocketData('admin', data);
            },
            servers(data) {
                this.setSocketData('servers', data);
            },
            frontend(data) {
                this.setSocketData('frontend', data);
            },
            install(data) {
                this.setSocketData('install', data);
            }
        },
        methods: {
            setSocketData(key, data) {
                const keyData = this.timelineData[key];
                const msg = [
                    ...(keyData || []), ...[
                        {
                            content: data,
                            timestamp: new Date().toLocaleString()
                        }
                    ]
                ];
                this.$set(this.timelineData, key, msg);
            },
            async start(){
                const res = await deploy({kw: this.deployForm.deploy});
                console.log('🚀 > start > res', res);
            }
        }
    };
    </script>
    
    <style scoped lang="scss">
    .category {
      box-sizing: border-box;
      margin: 24px;
    }
    .log-content {
      display: flex;
      flex-wrap: wrap;
    }
    .box-card {
       width: 500px;
       margin: 20px;
    }
    </style>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    //admin-blog/src/api/deploy.js
    import request from '@/utils/request';
    
    // 设置部署项目
    export function deploy(data) {
        return request({
            url: '/deploy',
            method: 'post',
            data
        });
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    演示

    查看演示视频


    看完记得 github点个star

  • 相关阅读:
    竣达技术 | 适用于”日月元”品牌UPS微信云监控卡
    vue使用swiper(轮播图)-真实项目使用
    ChatGPT Plus的Vision升级是一个改变游戏规则的创举
    自然语言处理NLP:tf-idf原理、参数及实战
    风电场视频监控:如何实现风电场可视化、智慧化管理模式?
    c++11的一些新特性
    vue - Vue脚手架
    矩阵小专题(矩阵快速幂+矩阵加速)
    面向对象的三大特征:封装、继承、多态
    时间序列论文: NeuralProphet: Explainable Forecasting at Scale
  • 原文地址:https://blog.csdn.net/zm06201118/article/details/127665682