前言
人生苦短, 我用 python~
作为一名专职前端开发的我, 为了帮助解决目前工作中的一些繁琐的工作 (主要是处理 Excel 数据), 解放程序员双手, 前阵子就刚刚入了 python 的坑, 毕竟也算是门工具语言, 都已经加入少儿编程了, 哈哈哈!
背景
实践是检验学习成果的唯一标准!
在我学习过程中, 一直琢磨着如何将学习的理论与我所掌握的知识结合起来, 来解决或者处理实际问题, 于是就有了 前端自动化打包部署 的念头.
尽快近几年, 市面上关于自动化部署的工具层出不穷, 比如当下比较流行的 Jenkins, 尽管如此, 我还是想自己试一试~
环境配置
初学乍道, 切不可眼高手低, 先给自己定个小目标, 先实现一个最简单版本.
工欲善其事, 必先利其器, 开发环境的配置是开发的第一步.
关于 python 的安装配置我就不赘述了.
为了方便测试, 我本地利用 VM 虚拟机安装了 CentOS 系统, 安装并配置 nginx 充当了服务器.
难点分析
要想实现打包, 核心需要考虑下面 2 个问题:
在 python 脚本中如何去执行前端的打包命令 NPM run build(这里以 vue 项目作为测试)
在 python 脚本中如何连接服务器将打包好的问题上传到服务器的指定目录中去
理论求证
通过查阅资料得知, python 中的 os 模块提供了非常丰富的方法用来处理文件和目录, 其中 os 模块中的 system() 函数可以方便地运行其他程序或者脚本, 其语法如下:
os.system(command)
command 要执行的命令, 相当于在 Windows 的 cmd 窗口中输入的命令. 如果要向程序或者脚本传递参数, 可以使用空格分隔程序及多个参数, 该方法返回结果如果为 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_file 和 close,close 方法的定义很简单, 直接调用 __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 文件 / 目录方法
来源: http://www.jianshu.com/p/eb9b0fa19bbc