前言
在一些小公司中,发布还处于手工部署的阶段。
对于曾经接触过自动化发布的笔者来说,这种“手动打包、手动上传”的效率实在太低。
但奈何公司的技术还停留在SVN时代,无法像以前在大学时使用Gitlab成套的CI/CD工具。
因此本文记录了用最原始的技术,借助JS脚本实现自动化部署的过程。
一、 原理
本文假设生产环境是Linux或FreeBSD环境,使用Nginx加载静态文件来完成网站的部署。
并且已经具备前置条件:Nginx已经设置好文件路径。
此时,如果我们手动上线一个项目,步骤是:
- 编译:
vite build
或ng build
- 打包:
zip -q -r dist.zip ./dist
- 图形化SCP终端连接服务器,上传文件
- 服务器上删除旧版本:
rm -rf projectName
- 解压:
unzip dist.zip
- 重命名:
mv dist projectName
而手写的自动化部署就是用一个脚本替我们执行这些命令。
二、探索
SCP2(失败)
如果你用中文关键词前端 自动发布
去搜索,排名靠前的全是使用名为 scp2 的npm插件:
抱着试一试的态度把别人的代码复制下来,然后运行,代码见外链 或 外链 或 外链
运行就遇到了问题:scp2上传没有任何反应(没有error),代码直接结束执行,文件没有上传。
Github上也有同样的问题:
由于SCP2这个包太小众了,并没有什么资料,而排名靠前的搜索结果都是用的SCP2,于是在这里卡了好久。
突然笔者发现:这个包最后更新是七年前......
已经是上古时代的产物了,怎么没早点看见:
于是第一个教训:NPM引用小众的包要看发布时间。
SSH2(可用)
SCP2行不通就只能换思路了,于是去看了一眼它基于的包:SSH2 , 而这个包是保持更新的:
所以去查文档,直接上代码了。
三、 实现
安装
npm install ssh2 --save-dev
引用
// 文件名为autoDepoly.js
const {Client} = require('ssh2');
const conn = new Client();
如果你使用的是Vite托管的VUE3,会提示require
用法已经不再支持,
我们可以在vite.config.js
中加入transformMixedEsModules
:
export default defineConfig({
// 加入下面几行
build: {
commonjsOptions: {
transformMixedEsModules: true,
},
}
})
这样就可以在vite中使用require
了。
来源:StackOverFlow
连接
这一步主要是创建一个SSH连接,写入主机名、端口、用户名、密码(或私钥)的过程。
/**
* 基本信息
* path: 项目文件夹的名称
* base: 项目文件夹所在的上一级目录
*/
const server =
{
host:"",
port:"",
username:"",
password:"",
path:"folderName/",
base:"/www/"
}
/**
* 建立连接
*/
conn.on('ready', () => {
// 这里是连接成功之后执行的代码
}).connect({
host: server.host,
port: server.port,
username: server.username,
// 如果使用私钥,把password改为:privateKey,即私钥的路径
password: server.password
});
基本用法——执行一条Bash命令
/**
* 执行bash命令
* 命令和Linux中一致
* stream.on-close表示执行完成后的操作
* on-data和stderr.on-data是监听执行过程中的数据
* 可以什么也不做,但必须带着后面两个回调函数
*/
conn.exec('你的bash命令', (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('执行完成');
}).on('data', (data) => {
console.log(data);
}).stderr.on('data', (data) => {
console.log(data);
});
});
组合——执行多条命令
实际上就是把第二条命令写在第一条命令执行后的回调里:
// 第一层
conn.exec('第一条bash命令', (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('第一条命令执行完成');
// 第二层
conn.exec('第一条bash命令', (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('第二条命令执行完成');
}).on('data', (data) => {
console.log( data);
}).stderr.on('data', (data) => {
console.log(data);
});
});
// 退出第二层
}).on('data', (data) => {
console.log(data);
}).stderr.on('data', (data) => {
console.log(data);
});
});
这种“套娃”写法看着有点乱,有一种优化方式就是把多条命令合起来写:
A && B && C
SFTP——上传文件
开头我们提到,有一个步骤就是上传文件,这一步的特殊性在于:
SSH都是在服务器上执行命令,而上传文件同时关系到本地和服务器。
SFTP就是基于SSH的文件传输协议,其写法如下:
/**
* 启动SFTP
* 上传使用fastPut方法,完成后回调
* 无论是否成功都是唯一的回调函数,如果成功err为null
* 只能上传一个文件,如果有文件夹需要压缩
*/
conn.sftp((err, sftp) => {
if (err) throw err;
console.log('SFTP启动,正在上传(等待时间可能较长)');
sftp.fastPut('./dist.zip', server.base + 'dist.zip', (err) => {
if (err) throw err;
console.log('上传完成');
});
});
最终的代码
把上面的代码拼起来,就是最终的功能了:
// 建立连接
conn.on('ready', () => {
console.log('SSH连接成功');
// 第一条命令
conn.exec('rm -rf ' + server.base + server.path, (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('删除历史版本完成');
// SFTP
conn.sftp((err, sftp) => {
if (err) throw err;
console.log('SFTP启动,正在上传(等待时间可能较长)');
// SFTP上传
sftp.fastPut('./dist.zip', server.base + 'dist.zip', (err) => {
if (err) throw err;
console.log('上传完成,即将解压');
// 第二条命令
conn.exec('unzip ' + server.base + 'dist.zip -d ' + server.base, (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('解压完成');
// 第三条命令
conn.exec('mv ' + server.base + 'dist ' + server.base + server.path + ';rm '+ server.base +'dist.zip;', (err, stream) => {
if (err) throw err;
stream.on('close', (code, signal) => {
console.log('重命名成功,即将断开连接,请打开网站查看更新是否正常');
conn.end();
}).on('data', (data) => {
console.log(data);
}).stderr.on('data', (data) => {
console.log(data);
});
})
}).on('data', (data) => {
console.log( data);
}).stderr.on('data', (data) => {
console.log(data);
});
})
});
});
}).on('data', (data) => {
console.log( data);
}).stderr.on('data', (data) => {
console.log(data);
});
});
}).connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password
});
提炼一下命令:
'rm -rf ' + server.base + server.path
删除历史项目文件夹:base+path等于完整路径'unzip ' + server.base + 'dist.zip -d ' + server.base
把zip解压到原来历史文件夹所在的目录'mv ' + server.base + 'dist ' + server.base + server.path + ';rm '+ server.base +'dist.zip;'
其实是两条命令:先把dist文件夹改名为项目文件夹,然后删除dist.zip文件
把这个文件扔在项目根目录,如果环境、参数都正确,使用node autoDepoly
就可以正常执行了(autoDepoly.js是文件名,换成你自己的),将会返回:
第一次执行很可能不成功,因为每个人的情况不一样,所以:
教训二:脚本的编写需要耐心,当出现问题时,需要拆分功能,逐个功能调试,定位问题
四、整合
到目前为止,已经实现“自动推送zip到服务器并解压”了
接下来我们要把编译压缩的过程也自动化了。
很简单,项目的package.json
中有快捷指令,比如运行npm run dev
实际上就是执行vite
因为项目构建的时候已经写好了:
我们只需要加一行:
"auto-depoly": "vite build && zip -q -r dist.zip ./dist && node autoDepoly.cjs && rm dist.zip"
这是很多条命令:
vite build
编译zip -q -r dist.zip ./dist
创建压缩文件node autoDepoly.js
执行自动部署rm dist.zip
删除压缩文件
注意:
- && 的作用是:当前一天命令执行成功时,后一条命令才会执行
- 文中的zip命令是Linux系统的,Windows需要换成等效的命令
至此,从编译、到打包、到上传、到解压,整个流程都是自动化执行了。
花费了一些时间,总算达成了目的。
五、总结
一点体会
对于大厂来说,已经实现了高度自动化和严格的规范,靠着公司内部的私有轮子就可以维持正常的开发活动,员工可以轻松学到前人踩过坑整理好的知识,甚至公司对于员工的成长会有所要求。
而在小公司中,前人的经验是一片空白,使用的工具也停留在远古时代,大多数的知识和经验都是自己去查,一点一点踩坑,最后总结出的东西还远远不如大厂的轮子。
客观上大厂和小厂的资源差距是相当大的,即使靠着更多的努力,也只能勉强弥补一部分环境的差距。