开发一个快捷发版工具~touch fish的时间又增加了

309 阅读5分钟

前言

作为一个前端开发,在日常开发工作中,当我们完成了一个功能的开发,需要发版到测试环境时,或多或少都会接触到一些发版工作的,在一些相对规范点的公司可能会配有CI/CD,那么我们直接推了代码,点立即构建即可。

但在相对小型一些的公司,因为各种条件限制吧,还是采用一些相对原始的方法,比如使用XShellXftpfinalShell等工具去发版,虽然就是打包完,一拖一粘贴,也不难,但当我们手头工作比较多,而测试又等着测时,就容易手忙脚乱,边打包边忙别的(比如在掘金mo学习~),等再次想起来要拖上去发版的时候,就已经不知道是什么时候了。

而且每次发版都要经历下面这些流程:

  • 打开finalShell
  • 找到发版地址
  • 找到云端环境前端文件夹的位置
  • 打开本地项目的打包文件夹,并将文件拖入finalShell中
  • 等待上传完毕,完成发版

这个流程多少还是有些繁琐,基于此,就有了这篇文章,我们可以自己编写一个发版的脚本工具,输入命令,即可完成发版,不再需要上面这一坨繁琐的步骤。

正文

连接工具——ssh2

首先,我们需要了解一个能够用于连接云端服务器的库——ssh2

这个库会为我们暴露一个Client构造函数,可以通过该构造函数创建连接的实例conn,调用connect方法同时传入配置信息,就可以连接到我们的云端服务器了。同时,我们可以监听connready事件,当完成连接后,去执行我们后续的操作——上传前端文件

const { Client } = require('ssh2');
const conn = new Client();

conn.on('ready', function() {
    ...
    // 连接建立后的回调函数
}).connect(
{
  host: '192.168.100.100',
  port: 22,
  username: 'root',
  password: '123456'
}
);

接下来,由于我们是想向云端环境去上传文件,而SFTP提供了安全性更高的文件传输方式,所以可以写出下面代码:

conn.sftp(function(err, sftp) {
      if (err) throw err;
      sftp.fastPut(本地文件地址,云端文件地址, (err) => {
      if(err) {
        console.log('错误' + err)
        return
      }
      console.log('上传成功')
    ...
})

当上传完毕之后,可以通过调用conn.end(),来断开与云端的连接。

到这里就完成了整个的连接,以及上传逻辑。但目前似乎使用fastPut方法一次只能上传一个文件,而且并不能上传整个文件夹,所以我们可以采用先将dist文件夹打成压缩包将压缩包上传到云端在云端进行解压这样一个操作来曲线救国~

压缩工具——archiver

这里再引入一个用于压缩文件的库——archiver

我们仿照官网,将路径信息配置进去即可。

var output = fs.createWriteStream(压缩文件的路径)
var archive = archiver('zip', {
    zlib: { level: 9 }
})
output.on('close', function() {
    resolve()
});
archive.on('error', function(err) {
    throw err;
});
archive.pipe(output);
// 配置第二个参数为false的话,会将内部文件打包,而不是将文件夹打包
archive.directory(要压缩的文件夹路径, false);
archive.finalize();

整合流程

前面这两个工具我们已经了解了,那接下来开始将整个的流程进行一个整合,来编写最终的脚本文件

再梳理一次:

  • 首先,在连接云端之前先将要上传项目打包文件压缩包准备好
  • 然后,通过ssh2与云端测试环境建立连接
  • 连接建立完成,通过sftp将压缩包进行上传
  • 上传完成后,在云端将压缩包进行解压
  • 解压完成,删除压缩包
  • 断开连接

代码实现:

// upload.js
const fs = require('fs');
const archiver = require('archiver');
const { Client } = require('ssh2');

// 删除本地压缩文件
function deleteFile(config) {
    return new Promise((resolve,reject) => {
        fs.access(__dirname + '/' + config.zipFileName, fs.constants.F_OK,(err) => {
            if(err) {
                resolve()
                return console.log('文件不存在')
            }
            fs.unlinkSync(__dirname + '/' + config.zipFileName)
            resolve()
        })
    })
}

// 压缩
function zipFile(config) {
    return new Promise((resolve,reject) => {
        var output = fs.createWriteStream(__dirname + '/' + config.zipFileName)
        var archive = archiver('zip', {
            zlib: { level: 9 }
        })
        output.on('close', function() {
            resolve()
        });
        archive.on('error', function(err) {
            throw err;
        });
        archive.pipe(output);
        // 配置第二个参数为false的话,会将内部文件打包,而不是将文件夹打包
        archive.directory(__dirname + '/' + config.fileDir + '/', false);
        archive.finalize();
    })
}

// 上传
function uploadFile(config) {
    return new Promise((resolve,reject) => {
        conn.on('ready', function() {
            console.log('Client ready');
            conn.sftp(function(err, sftp) {
              if (err) throw err;

              // 业务逻辑
              sftp.fastPut(`${__dirname}/${config.zipFileName}`,`${config.remoteDir}/${config.zipFileName}`,
                (err) => {
                    if (err) {
                        console.log('错误' + err)
                        return
                    }
                    console.log('上传成功')
                    // 执行解压命令
                    conn.exec(`unzip -o ${config.remoteDir}/${config.zipFileName} -d ${config.remoteDir}`, (err,stream) => {
                        if(err || !stream) {
                            reject('失败')
                        }else{
                            stream.on('close', () => {
                                console.log('上传完成')
                                resolve()
                            }).on('data', (data) => {
                                console.log(data.toString())
                            })
                        }
                    })
                })
            });
          }).connect(config);
    })
}

// 删除压缩包文件
async function removeZip(config) {
    return new Promise((resolve,reject) => {
        conn.exec(`rm -rf ${config.remoteDir}/${config.zipFileName}`, (err,stream) => {
            if(err || !stream) {
                reject('失败')
            }else{
                stream.on('close', () => {
                    console.log('删除完成')
                    resolve()
                }).on('data', (data) => {
                    console.log(data.toString())
                })
            }
        })
    })
    
}

// 汇总
async function handle(config) {
    if(!config.host) {
        console.log('配置错误')
        return
    }
    await deleteFile(config)
    await zipFile(config)
    await uploadFile(config)
    await removeZip(config)
    conn.end()
    console.log('打包并上传完成')
}   

// 配置对象
let finalConfig = {
    host: '192.168.100.100',
    port: 22,
    username: 'root',
    password: '123456',
    remoteDir: '/home/web',
    zipFileName: 'dist.zip',
    fileDir: '../hello-world/dist'
}
// 执行
handle(finalConfig)

当执行这个脚本文件,node upload.js后,就可以在控制台看到:

image.png

同时在云端服务器上也能看到,已经上传成功了:

image.png

补充

那么,到这里我每次只需要打开控制台然后执行脚本就可以了,确实节省了很大一部分操作。但如果我们同时维护的系统有多个该怎么办呢?

这里可以在执行的脚本命令后加参数,比如node upload.js sys1,而在脚本中,通过process.argv[2]就可以获取到键入的参数了,这样就可以去配置更多的项目发版地址喽~

let finalConfig = {}
let x = process.argv[2]
if(x == 'sys1') {
    finalConfig = sysConfig1
}else if(x == 'sys2') {
    finalConfig = sysConfig2
}
...
handle(finalConfig)

甚至可以再简单一点,把执行脚本的命令做成一个cmd文件,这样连手动唤起命令行都不用了,用鼠标点开cmd文件,输入要发版的系统,就可以自动去读取对应的配置,帮我们完成发版了~

@echo off
chcp 65001
set /p UserInput=请输入要发版的项目: 
node ./upload.js %UserInput%
image.png

最后附上这个发版工具的体验地址,需要的掘友可以试一下,在原有的基础上也可以做更多有意思的操作~