前言
人生苦短,我用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
;