前言

最近忙于换公司,所以笔记好长时间没写,光忙着去刷leetcode算法和面试题了,现在工作正式换好了,后面有时间可以把新工作中遇到的相关解决思路记录下来了。

刚来第一天,熟悉工程阶段,发现新工作负责的前端项目,整个构建发布流程过于繁复,大概需要:本地构建 -> 登录到服务器 -> 服务器上新建新的目录 -> 将本地构建传到此目录下 -> 构建docker镜像 -> 给该镜像打tag -> 推送到镜像服务器 -> Rancher上修改tag号重新部署服务

整个过程其实是缺少了jekins或者bamboo之类的自动构建部署工具,所以整个流程需要手动执行,问题主要是步骤繁琐、命令复杂、版本号命名、docker镜像构建推送复杂等。

据我来的这两天观察来看,dev环境平均每天都要发版1~3次左右,uat环境每周都会有发版,也就是说发布相对频繁,重复性工作过多,极大地浪费了开发人员的时间,所以决定用Node.js将重复性工作接管,减少开发人员的机械性工作。

思路

首先,明确服务器上不能搭建jekins或其他自动化构建部署工具(服务器资源紧张,没必要因为单独一个项目搭建自动化工具)。

本地构建npm run build以及最后一步Rancher部署工作,属于非关联流程(最后一步其实可以通过webhooks也加入流程,但是在未想好回退机制前,此步骤先不纳入流程中)。

主要流程就是:登录到服务器 -> 服务器上新建新的目录 -> 将本地构建传到此目录下 -> 构建docker镜像 -> 给该镜像打tag -> 推送到镜像服务器

通过这个步骤,可以得知,程序至少要实现:获取用户输入ssh连接执行shell命令ftp上传等功能。

开发

  • 日志输出:chalk

    通过引入chalk来做到日志颜色的输出来区分错误、成功以及提示性消息。

    使用方式:

    1
    2
    3
    console.log(chalk.red.bold('错误日志'));
    console.log(chalk.green.bold('成功日志'));
    console.log(chalk.blue.bold('提示日志'));
  • 获取用户命令行输入:readline

    使用readline来捕获命令行输入,封装一个方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 同步获取问题
    * @param {*} question
    * @returns
    */
    function readSyncByRl(question) {
    return new Promise((resolve) => {
    const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    });
    rl.question(question || '>', (answer) => {
    rl.close();
    resolve(answer.trim());
    });
    });
    }

    获取用户输入:

    1
    2
    3
    4
    const host = await readSyncByRl('请输入服务器地址:');
    const port = await readSyncByRl('请输入服务器端口号:');
    const username = await readSyncByRl('请输入服务器账号:');
    const password = await readSyncByRl('请输入服务器密码:');
  • 执行shell命令ssh2

    使用ssh2来连接目标服务器,同时执行shell命令

    封装方法:

    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
    /**
    * 连接服务器
    * @param {host, port, username, password} server
    * @param {*} then
    */
    function doConnect(server, then) {
    const conn = new Client();
    conn.on('ready', () => {
    console.log(chalk.green.bold(`服务器[${server.host}]连接成功!`));
    then && then(conn);
    }).on('error', (err) => {
    console.error(chalk.red.bold(`服务器[${server.host}]连接失败!`), err)
    }).on('close', () => {
    conn.end()
    }).connect(server);
    }

    /**
    * 执行shell命令
    * @param {host, port, username, password} server
    * @param {*} cmd
    * @param {*} then
    */
    function doShell(server, cmd, then) {
    doConnect(server, (conn) => {
    conn.shell((err, stream) => {
    if (err) throw err;
    let res = '';
    stream.on('close', () => {
    conn.end();
    then && then(res);
    }).on('data', (data) => {
    res += data;
    console.log(res);
    }).stderr.on('data', (data) => {
    console.log(chalk.red.bold(`执行Shell发生错误:${data}`))
    });
    if (cmd instanceof Array) {
    stream.end(`${cmd.join(' && ')}\nexit\n`);
    } else {
    stream.end(`${cmd}\nexit\n`);
    }
    });
    })
    }

    使用:

    1
    doShell({ host, port, username, password }, `mkdir -p ${remotePath}`, () => console.log(`新建tag目录${remotePath}成功`));
  • 使用sftp上传文件:ssh2-sftp-client

    这里需要说明的是,ssh2也能上传文件,但是它不支持上传文件夹,只能上传文件,因此,如果不使用该npm库de话,需要手动实现文件夹的迭代循环,以及调用新建文件夹shell命令,过程相对繁琐,所以引入了一个新的npm库来处理上传

    封装方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 上传
    * @param {*} localPath
    * @param {*} remotePath
    * @param {*} then
    */
    async function doUpload(localPath, remotePath) {
    const tempPath = path.resolve(__dirname, `../${localPath}`);
    try {
    const stat = fs.statSync(tempPath);
    if (stat.isFile()) {
    console.log(`上传文件${localPath}...`);
    return sftp.put(tempPath, `${remotePath}/${localPath}`);
    }
    if (stat.isDirectory()) {
    console.log(`上传文件夹${localPath}...`);
    return sftp.uploadDir(tempPath, `${remotePath}/${localPath}`);
    }
    } catch(err) {
    console.log(chalk.red.bold(`【${localPath}】上传失败, 原因:`, err));
    process.exit(0);
    }
    }

    使用方法如下:

    1
    2
    3
    4
    await sftp.connect(server);
    await doUpload('dist', remotePath);
    console.log(chalk.green.bold('文件上传成功'));
    sftp.end();

其他

  • 更改文件格式

    其中上传的文件中,有一个shell脚本,此脚本上传后,需要修改其文件格式,否则执行会失败,更改方式如下:

    1
    sed -i 's/\r$//g' docker/setenv.sh
    • 密码输入

    执行sudo命令或者docker login的时候,都需要输入密码,但是在脚本中,显然没办法捕获输入,可以使用管道符来完成:

    1
    2
    echo ${password} | sudo -S docker build -t ${preTag}:v${tag} .
    echo ${dockerpassword}|sudo docker login --username ${dockerusername} --password-stdin ${docker}

完整代码

因为涉及到了公司项目,所以完整代码不放出来了,核心逻辑及难点已经写完,剩下的,都是边边角角,很容易了