基于Python实现前端自动化打包部署

前言

人生苦短,我用python~

作为一名专职前端开发的我,为了帮助解决目前工作中的一些繁琐的工作(主要是处理 excel 数据),解放程序员双手,前阵子就刚刚入了 python 的坑,毕竟也算是门工具语言,都已经加入少儿编程了,哈哈哈!

背景

实践是检验学习成果的唯一标准!

在我学习过程中,一直琢磨着如何将学习的理论与我所掌握的知识结合起来,来解决或者处理实际问题,于是就有了 前端自动化打包部署 的念头。

尽快近几年,市面上关于自动化部署的工具层出不穷,比如当下比较流行的Jenkins,尽管如此,我还是想自己试一试~

环境配置

初学乍道,切不可眼高手低,先给自己定个小目标,先实现一个最简单版本。

工欲善其事,必先利其器,开发环境的配置是开发的第一步。

关于 python 的安装配置我就不赘述了。

为了方便测试,我本地利用 VM 虚拟机安装了 centos 系统,安装并配置 nginx 充当了服务器。

难点分析

要想实现打包,核心需要考虑下面2个问题:

  • python 脚本中如何去执行前端的打包命令npm run build(这里以vue项目作为测试)
  • python 脚本中如何连接服务器将打包好的问题上传到服务器的指定目录中去

理论求证

通过查阅资料得知,python中的 os 模块提供了非常丰富的方法用来处理文件和目录,其中 os模块中的system()函数可以方便地运行其他程序或者脚本,其语法如下:

os.system(command)

command 要执行的命令,相当于在Windowscmd窗口中输入的命令。如果要向程序或者脚本传递参数,可以使用空格分隔程序及多个参数,该方法返回结果如果为 0,则表示命令执行成功,其它值则表示错误。

这样就解决了第一个问题。

关于服务器连接这一块,可以使用python的一个第三方模块 paramiko,它实现了SSHv2协议,允许我们直接使用SSH协议对远程服务器执行操作,关于 paramiko 的更多知识和用法,请戳这里

这样上面两个难点就解决了,我们就可以开工了。

小试牛刀

首先定义一个类 SSHConnect 后续的方法我们都会在这个类里面完善

class SSHConnect:
    # 定义一个私有变量,用来保存ssh连接通道,初始化为None
    __transport = None

初始构造函数

我们需要在构造函数中定义我们需要的参数,初始化我们的连接

# 初始化构造函数(主机,用户名,密码,端口,默认22)
def __init__(self, hostname, username, password, port=22):
    self.hostname = hostname
    self.port = port
    self.username = username
    self.password = password
    # 创建 ssh 连接通道
    self.connect()

建立 ssh 连接通道

我们在构造函数中最后调用了一个 connect 方法建立 ssh 连接通道,现在我们来具体的实现它

# 建立ssh连接通道,并绑定在 __transport 上
def connect(self):
    try:
        # 设置SSH连接的远程主机地址和端口
        self.__transport = paramiko.Transport((self.hostname, self.port))
        # 通过用户名和密码连接SSH服务端
        self.__transport.connect(username=self.username, password=self.password)
    except Exception as e:
        # 连接出错
        print(e)

执行打包

现在我们需要创建一个打包方法,执行 npm run build 命令,利用我们 os.system 方法,入参是 work_path 是打包项目所在的目录

# 前端打包(入参work_path为项目目录)
def build(self, work_path):
    # 开始打包
    print('########### run build ############')
    # 打包命令
    cmd = 'npm run build'
    # 切换到需要项目目录
    os.chdir(work_path)
    # 当前项目目录下执行打包命令
    if os.system(cmd) == 0:
        # 打包完成
        print('########### build complete ############')

只有一点要注意,就是要通过 os.chdir(work_path) 方法切换到项目的所在目录,打包当前的项目。

文件上传

打包结束后,我们需要将打包好的 dist 文件夹下的文件上传到服务器,因此,我们需要创建一个文件上传方法,我们通过 paramiko.SFTPClient 方法创建 sftp 来完成

该方法入参需要两个参数,一个是本地项目打包后的dist路径 local_path,另一个是要上传到服务器的目标目录 target_path

# 文件上传
def upload(self, local_path, target_path):
    # 判断路径问题
    if not os.path.exists(local_path):
        return print('local path is not exist')

    print('文件上传中...')

    # 实例化一个 sftp 对象,指定连接的通道
    sftp = paramiko.SFTPClient.from_transport(self.__transport)
    # 打包后的文件路径
    local_path = os.path.join(local_path, 'dist')
    # 本地路径转换,将windows下的 \ 转成 /
    local_path = '/'.join(local_path.split('\\'))
    # 递归上传文件
    self.upload_file(sftp, local_path, target_path)

    print('文件上传完成...')
    # 关闭连接
    self.close()

为了方便操作,需要将 windows 中的路径分隔符\转成 linux 下的分隔符/

此外,该方法中调用了另外两个方法,分别是 upload_filecloseclose 方法的定义很简单,直接调用 __transport.close() 方法即可

# 关闭连接
def close(self):
    self.__transport.close()

考虑到我们的 static 不是文件,而是一个文件夹,因此需要递归遍历,并将其拷贝到服务器上,所以我们定义了upload_file 方法,专门负责这个事情。

执行linux命令

上面我们也提到了,需要递归遍历static并上传到服务器,那么上传到服务器的目录结构肯定需要跟原来的 static 保持一致,因此对服务器的操作肯定是不可少的,需要执行linux命令,我们需要一个 exec 方法来实现这个功能,入参就是 linux 命令

# 执行linux命令
def exec(self, command):
    
    # 创建 ssh 客户端
    ssh = paramiko.SSHClient()
    # 指定连接的通道
    ssh._transport = self.__transport
    
    # 调用 exec_command 方法执行命令
    stdin, stdout, stderr = ssh.exec_command(command)
    
    # 获取命令结果,返回是二进制,需要编码一下
    res = stdout.read().decode('utf-8')
    # 获取错误信息
    error = stderr.read().decode('utf-8')
    
    # 如果没出错
    if error.strip():
        # 返回错误信息
        return error
    else:
        # 返回结果
        return res
    

现在你可以连接服务器测试一下,这个方法的正确性

    ssh = SSHConnect(hostname='x.x.x.x', username='root', password='xxx')
    print(ssh.exec(r'df -h'))

我们连接到服务器并尝试调用 linux 中的 df -h 命令查看我们系统文件系统的磁盘使用情况,不出意外的话,会看到控制台返回的信息

ps:命令 df -h 前面的 r 是为了让python解释器不转义

递归上传文件

准备工作做好以后,我们就可以来是实现我们的递归上传的方法 upload_file 了,主要是通过前面创建的 sftp 对象的 put 方法,将本地文件上传到对应的服务器中

# 递归上传文件
def upload_file(self, sftp, local_path, target_path):
    # 判断当前路径是否是文件夹
    if not os.path.isdir(local_path):
        # 如果是文件,获取文件名
        file_name = os.path.basename(local_path)
        # 检查服务器文件夹是否存在
        self.check_remote_dir(sftp, target_path)
        # 服务器创建文件
        target_file_path = os.path.join(target_path, file_name).replace('\\', '/')
        # 上传到服务器
        sftp.put(local_path, target_file_path)
    else:
        # 查看当前文件夹下的子文件
        file_list = os.listdir(local_path)
        # 遍历子文件
        for p in file_list:
            # 拼接当前文件路径
            current_local_path = os.path.join(local_path, p).replace('\\', '/')
            # 拼接服务器文件路径
            current_target_path = os.path.join(target_path, p).replace('\\', '/')
            # 如果已经是文件,服务器就不需要创建文件夹了
            if os.path.isfile(current_local_path):
                # 提取当前文件所在的文件夹
                current_target_path = os.path.split(current_target_path)[0]
            # 递归判断
            self.upload_file(sftp, current_local_path, current_target_path)

上述方法中添加了一个 check_remote_dir 方法,用来检测服务器端是否已经存在了文件夹,如果服务端没有我们就创建一个,定义如下:

# 创建服务器文件夹
def check_remote_dir(self, sftp, target_path):
    try:
        # 判断文件夹是否存在
        sftp.stat(target_path)
    except IOError:
        # 创建文件夹
        self.exec(r'mkdir -p %s ' % target_path)

非常巧妙的利用了 sftp.stat 方法查看文件信息来区分的。

合并流程,自动发布

现在基本的方法我们都已经实现了,接下来我们需要将它们合并到 auto_deploy 方法中,真正实现自动发布。

# 自动化打包部署
def auto_deploy(self, local_path, target_path):
    # 打包构建
    self.build(local_path)
    # 文件上传
    self.upload(local_path, target_path)

ok~ 我们来调用 auto_deploy 方法测试一下:

if __name__ == '__main__':
    # 项目目录
    project_path = r'D:\learn\vue-demo'
    # 服务器目录
    remote_path = r'/www/project'
    
    # 实例化
    ssh = SSHConnect(hostname='x.x.x.x', username='root', password='xxx')
    # 自动打包部署
    ssh.auto_deploy(project_path, remote_path)

如果一切顺利,就可以看到控制台输出成功!!

再看看服务器文件是否已上传成功。

并尝试访问我的主页!

非常完美!

Congratulations! 你已经 get 了这项技能,点个赞吧!

服务器清空

到这里的话,我们的功能已基本完成了,只是还有一个小小的问题遗留,如果我们不断的迭代优化,那么如果我们不清除服务器的目录的话,会堆积越来越多的旧的文件,占用服务器的空间,因此我们需要在打包上传前清空一下。

不妨定义一个clear_remote_dir方法来实现这个功能

# 清空文件夹
def clear_remote_dir(self, target_path):
    if target_path[-1] == '/':
        cmd = f'rm -rf {target_path}*'
    else:
        cmd = f'rm -rf {target_path}/*'
    self.exec(cmd)

之后把它添加到 auto_delpoy 方法中 self.upload 之前就好了~

结语

勉强算是完成了一个小工具吧,这个过程对我来说也算是对 python 的一次小小的实践吧,也算是颇有收获了。

对于上述的代码,完全还可以通过 sys.argv 通过命令行参数解析的方式来实现真正的 cmd 调用,笔者在这里就不赘述了,感兴趣的小伙伴可以自己去实践一下。

可以看到python 在语法上的简洁和优雅,这一点也是让我感觉还是挺舒服的,对我个人来说,可能后面更多是作为一门工具语言来使用,最大程度的去解决实际问题。

完整代码请戳

参考资料

Python模块学习 - Paramiko

Python3 OS 文件/目录方法

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容