轻量化前端更新方案

前言

​ 一句话介绍:它是可以一行命令将代码更新到服务器的脚本

​ 轻量级更新方案最开始源于掘金的文章,后来我们从零实现了一个更新脚本,并且已在是生产环境中进行使用很长时间,算是非常稳定的版本,个人认为轻量化更新方案是非常使用小型开发团队

​ 现在我们切换到了gitlab的CI/CD。所以这种方案已经不再是我们的主流方案,但是一路使用过来,非常稳定的解决了更新问题,还是非常不错的

优点: 快速,稳定,自动备份指定文件夹(灵活性高,但是需要自己实现)

缺点:需要手动回滚(自动回滚需要编码),相对来说没那么规范,没有留下记录,敏感数据存储在电脑中,配置文件可以git忽略

核心流程

  1. 确认并打包项目
  2. 通过node-ssh连接线上服务器
  3. 将打包代码指定名称进行压缩
  4. 备份之前的代码,删除以前的代码包,并解压压缩包
  5. 删除本次打包代码,断开ssh链接

如何使用

代码地址

  1. 将仓库文件放入项目

  2. 安装依赖

1
npm install node-ssh inquirer archiver -D
  1. 修改upload.config.js内容
  2. 增加脚本命令
1
"upload": "node build/upload.js"
  1. 运行命名,验证是否功能正常

核心代码

build/upload.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
const fs = require('fs')
const path = require('path')
const { NodeSSH } = require('node-ssh')
const archiver = require('archiver')
const inquirer = require('inquirer')
const exec = require('child_process').exec
const ssh = new NodeSSH()
const uploadFun = require('../upload.js')

/**
* 获取当前平台
*/
let objName = process.argv[2] // 更新名字
let startTime = null // 程序开始更新的时间
// 获取上传服务器配置
let config = uploadFun(objName)
const verifyList = [
{
type: 'input',
message: '您正在更新到线上环境,请确认接口域名',
name: 'objName',
},
]
inquirer.prompt(verifyList).then(() => {
uploadBuild()
})

function uploadBuild() {
startTime = new Date()
console.log(`${objName}开始更新`)
let buildcmd = exec(config.buildScript, (error, stdout, stderr) => {
if (!error) {
console.log('打包完成', stdout)
app()
} else {
console.error('打包出现错误', stderr)
process.exit(0)
}
})
buildcmd.stdout.on('data', (data) => {
console.log(data.toString())
})
}

/**
* 通过ssh链接服务器
*/
function app() {
ssh
.connect({
host: config.host,
username: config.username,
password: config.password,
})
.then((res) => {
// 上传代码压缩包
uploadData()
})
.catch((err) => {
console.log(err)
})
}

/**
* 上传代码 压缩现有代码
*/
function uploadData() {
// 创建文件输出流
let output = fs.createWriteStream(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`)
// 设置压缩级别
let archive = archiver('zip', {
zlib: {
level: 8,
},
})
// 存档警告
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
console.warn('stat故障和其他非阻塞错误')
} else {
throw err
}
})
// 存档出错
archive.on('error', function(err) {
throw err
})
// 通过管道方法将输出流存档到文件
archive.pipe(output)
archive.directory(`${path.join(__dirname, '../')}${config.buildPath}/${config.buildobj}`, '/')
archive.finalize()
// 文件输出流结束
output.on('close', function() {
console.log(`总共 ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB,完成源代码压缩`)
ssh
.putFile(
`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`,
`${config.uploadDir}/${config.objname}.zip`
)
.then(() => {
console.log('程序zip上传成功,判断线上是否需要备份')
runcmd()
})
.catch((err) => {
console.log(err)
})
})
}

/**
* 执行ssh命令 判断当前是否存在备份
*/
function runcmd() {
ssh
.execCommand('ls', {
cwd: config.uploadDir,
})
.then((res) => {
if (res.stdout) {
let fileList = res.stdout.split('\n')
if (config.objname == config.backObject) {
if (fileList.includes(config.objname)) {
console.log('当前更新为线上正常环境,开始进行备份')
backupData()
} else {
console.log('当前更新为线上正常环境,并且是第一次,将跳过备份')
cmdunzip()
}
} else {
console.log('当前为测试环境,无需备份,直接解压上传压缩包')
cmdunzip()
}
} else if (res.stderr) {
console.log('查询指定目录失败')
} else {
console.log('ssh链接发生了错误')
}
})
}

/**
* 备份项目
*/
function backupData() {
ssh
.execCommand(`mv ${config.objname} backup/${config.objname}_backup${new Date().getTime()}`, {
cwd: config.uploadDir,
})
.then((res) => {
if (res.stderr) {
console.log('备份发生错误', res.stderr)
} else {
console.log('完成备份,解压最新代码')
cmdunzip()
}
})
.catch((err) => {
console.log('备份发生未知链接错误', err)
})
}

/**
* 解压最新代码zip
*/
function cmdunzip() {
// 解压程序
ssh
.execCommand(
`rm -rf ${config.objname} && unzip -o -d ${config.uploadDir}/${config.objname} ${config.objname}.zip && rm -f ${config.objname}.zip`,
{
cwd: config.uploadDir,
}
)
.then(() => {
console.log(`项目包完成解压,${config.objname}项目部署成功了!`)
console.log(`项目更新时长${(new Date().getTime() - startTime.getTime()) / 1000}s`)
return deletelocalFile().then(() => {
console.log('本地缓存zip清除完毕')
})
})
.then(() => {
ssh
.execCommand(`rm -rf ${config.objname}/static/.DS_Store`, {
cwd: config.uploadDir,
})
.then(() => {
console.log('线上项目.DS_Store删除完成')
ssh.dispose()
process.exit(0)
})
.catch((err) => {
console.log(err)
})
})

.catch((err) => {
console.log('解压出现错误', err)
})
}
/**
*删除本地生成的压缩包
*/
function deletelocalFile() {
return new Promise((resolve, reject) => {
fs.unlink(`${path.join(__dirname, '../')}${config.buildPath}/${config.objname}.zip`, (err) => {
if (err) {
reject(err)
throw err
} else {
resolve()
}
})
})
}

配置文件

upload.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 打包核心配置文件
let Available = ['dist-a', 'dist-b'] // dist-a 环境a代码包 dist-b 环境b代码包 npm run upload dist-a
/**
* 获取更新配置
* @param {String} objName 当前更新名称
* @returns
*/
module.exports = (objName) => {
if (!Available.includes(objName)) {
console.log('当前项目不存在您输入的更新命令,请检查更新名称')
process.exit(0)
}
return {
host: 'xx.xx.xx.xx', // 服务器地址
username: 'root',
password: 'xxxxxxxxxx',
buildPath: '', // 本地打包项目地址(多层路径用这个)
buildobj: 'dist', // 本地打包文件名称
uploadDir: '/xx/xx/xx', // 服务端项目地址
objname: objName, // 打包项目名称
backObject: 'objName', // 备份的文件夹名称
buildScript: 'npm run build' // 更新命令
}
}

触发命令

最后在package.json增加一行命令,运行前面的脚本文件

1
2
3
4
"scripts": {
// ......
"upload": "node build/upload.js"
},

依赖版本

​ 因为更新脚本是在项目里面的,所以需要额外安装依赖

推荐版本号

1
2
3
"node-ssh": "^12.0.0",
"inquirer": "^7.3.3",
"archiver": "^3.1.1",

实际使用

1
npm run upload xxxx // 线上代码文件夹名称

upload 命令后面的字符串就是服务器上的文件夹名称,这里为了防止更新命名敲错了,需要首先在upload.config.js中进行更新白名单声明,如果配置都正确的情况下,你就可以看到,这就代表成功了~

最后

​ 脚本文件还存在很高的上限,可以优化一下备份部分的备份代码生成规则,再增加一个回滚代码的脚本,就可以实现线上的无感知回滚了

​ 如果使用中遇到了什么问题,请到QQ群 530496237,一起吹吹水