当一套前端代码需要根据配置打包成不同版本且部署到不同环境时,如果还是手动完成这些事情,这无疑是非常浪费时间的。这个时候自动打包、自动部署脚本就应运而生了。

前端

NodeJS、Angular13

需求

一套前端代码,打包时可以选择企业版、企业版深色、社区版、社区版深色等版本,部署时可以选择01、02、03、04等环境

自动打包

对于 当执行打包、部署命令时 提示版本选择、环境选择这一功能,可以使用Linux脚本实现,也可以使用inquirer实现。但由于inquirer是基于node,意味着选择版本之后也需要使用nodejs代码启动打包命令ng build,虽然能够通过child_process(具体可参考这里)实现,但经过测试,不能输出打包日志,所以放弃了这种方案,改用Linux脚本实现。

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
#!/bin/bash

PS3="请选择版本: "
options=("Enterprise" "Enterprise-Dark" "Community" "Community-Dark")

selected="" # 初始化selected变量

select opt in "${options[@]}"; do
selected="$opt" # 将选中的项赋值给selected变量
case $opt in
"Enterprise")
# echo "你选择了Enterprise"
break
;;
"Enterprise-Dark")
# echo "你选择了Enterprise-Dark"
break
;;
"Community")
# echo "你选择了Community"
break
;;
"Community-Dark")
# echo "你选择了Community-Dark"
break
;;
*) echo "无效的选项";;
esac
done

echo "你选择的版本是: $selected" # 打印出selected变量的值

rm -rf ./dist/ui.zip ./dist/admin.zip
# 先打包后台管理界面
ng build admin --base-href=/admin/ --configuration=$selected
# 再打包前端界面
ng build --configuration=$selected
cd dist
# 将后台管理界面拷贝到前端界面目录下(服务器通过Nginx代理到不同界面)
mv admin ui/
zip -qr ui.zip ui/

对于Angular工程,我们可以通过在environments/environment.ts中自定义变量的方式控制不同的版本,形如这样:

1
2
3
4
export const environment = {
production: false,
versionType: 'Enterprise-Dark'
};

但是在打包时如何更改versionType的值呢?是不是可以这样ng build --env.versionType=xxx?很遗憾,Angular CLI并不支持这样,ng build的参数是不能自定义的。虽然这样,我们仍然可以通过如下两种方式达到目的:

  • ng build之前通过nodejs读写文件的方式更改environments/environment.prod.tsversionType的值
  • 通过配置angular.json中的configurations,然后指定ng build --configuration=xxx来控制打包时使用哪个配置信息即可,形如这样:
    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
    "configurations": {
    "Enterprise": {
    "fileReplacements": [
    {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.enterprise.ts"
    }
    ],
    "outputHashing": "all"
    },
    "Enterprise-Dark": {
    "fileReplacements": [
    {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.enterprise-dark.ts"
    }
    ],
    "outputHashing": "all"
    },
    "Community": {
    "fileReplacements": [
    {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.community.ts"
    }
    ],
    "outputHashing": "all"
    },
    "Community-Dark": {
    "fileReplacements": [
    {
    "replace": "src/environments/environment.ts",
    "with": "src/environments/environment.community-dark.ts"
    }
    ],
    "outputHashing": "all"
    }
    }

自动部署

自动部署也是有两种方案:sshpassnode-ssh

  • sshpass

1
2
3
4
5
6
7
8
#!/bin/bash

server=""
username=""
password=""

sshpass -p $password scp ./dist/ui.zip $username@$server:/data/app/
sshpass -p $password ssh $username@$server 'cd /data/app/; sh ui_update.sh'

这种方案在基于Intel芯片的Mac上测试成功了,但是在搭载Apple芯片的Mac上执行不成功,暂时不知道什么原因。

  • node-ssh

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
const path = require('path')//引入模块
const inquirer = require('inquirer')// 询问消息npm install --save inquirer@^8.0.0
const ora = require('ora')// 加载动画ora6.0以上版本不支持require方式引入,如果在node中使用,需要使用5.0版本
const { NodeSSH } = require('node-ssh')//链接ssh模块
const util = require('./util')//引入样式

const ssh = new NodeSSH()//创建实例ssh
var configGlobal// 声明环境对象在选择部署环境时赋值

// 连接到服务器
async function connectServer() {
const spinner = ora('登录服务器中...\n')
spinner.start()
ssh.connect(configGlobal.config)
.then(() => {
spinner.stop()
util.green('SSH登录成功')
mvRemoteFile()
}).catch((err) => {
spinner.stop()
util.red('SSH登录失败:\n', err)
})
}

// 备份远程文件
async function mvRemoteFile() {
await ssh.execCommand(
`mv ui.zip ui_${util.nowDate()}.zip`,
{ cwd: configGlobal.remoteFileRoot }
).then(() => {
util.green('远程文件 ui.zip 备份成功')
rmRemoteFile()
})
}

// 删除远程文件ui.zip
async function rmRemoteFile() {
await ssh.execCommand(
`rm -rf ui`,
{ cwd: configGlobal.remoteFileRoot }
).then(() => {
util.green('远程文件 ui 删除成功')
uploadFile()
})
}

// 上传文件到服务器
async function uploadFile() {
const spinner = ora('上传文件到服务器...\n')
spinner.start()
await ssh.putFile(`${path.join(process.cwd())}/dist/ui.zip`, `${configGlobal.remoteFileRoot}ui.zip`)
.then(() => {
spinner.stop()
util.green('本地文件 ui.zip 上传成功')
unzipRemoteFile()
}).catch((err) => {
spinner.stop()
util.red('本地文件 ui.zip 上传失败:\n', err)
})
}

// 解压远程文件ui.zip
async function unzipRemoteFile() {
await ssh.execCommand(
`unzip ui.zip`,
{ cwd: configGlobal.remoteFileRoot }
).then(() => {
util.green('远程文件 ui.zip 解压成功')
configGlobal.restart ? execRemoteShell() : end()
})
}

// 执行远程命令
async function execRemoteShell() {
const spinner = ora('服务重启中...\n')
spinner.start()
await ssh.execCommand(
`sh restart.sh`,
{ cwd: configGlobal.remoteFileRoot }
).then(() => {
util.green('服务重启成功')
end()
})
}

function end() {
util.green('UI部署完成')
ssh.dispose()
}

const username = '', password = ''

const envSetting = [
{
envName: '01环境',
remoteFileRoot: '/data/app/',
config: {
host: '',
port: 22,
username: username,
password: password
}
},
{
envName: '02环境',
remoteFileRoot: '/data/app/',
config: {
host: '',
port: 22,
username: username,
password: password
}
},
{
envName: '03环境',
remoteFileRoot: '/data/app/',
config: {
host: '',
port: 22,
username: username,
password: password
},
restart: true
},
{
envName: '04环境',
remoteFileRoot: '/data/app/',
config: {
host: '',
port: 22,
username: username,
password: password
},
restart: true
}
];

// 启动 自执行函数
(async function () {
const choices = envSetting.map(item => {
return item.envName
})
inquirer.prompt([
{
type: 'list',
message: '请选择部署环境:',
name: 'environment',
default: '',
// 前缀
prefix: '☆',
// 后缀
suffix: '',
choices
}
]).then(res => {
configGlobal = envSetting[choices.indexOf(res.environment)]
connectServer()
})
})()

./util.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
exports.underLine = (value) => {
console.log(`\u001b[21m${value}\u001b[0m`);
}

exports.gray = (value) => {
console.log(`\u001b[30m${value}\u001b[0m`);
}

exports.red = (value) => {
console.log(`\u001b[31m${value}\u001b[0m`);
}

exports.green = (value) => {
console.log(`\u001b[32m${value}\u001b[0m`);
}

exports.yellow = (value) => {
console.log(`\u001b[33m${value}\u001b[0m`);
}

exports.blue = (value) => {
console.log(`\u001b[34m${value}\u001b[0m`);
}

exports.purple = (value) => {
console.log(`\u001b[35m${value}\u001b[0m`);
}

exports.blueSky = (value) => {
console.log(`\u001b[36m${value}\u001b[0m`);
}

exports.white = (value) => {
console.log(`\u001b[37m${value}\u001b[0m`);
}

exports.nowDate = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const today = now.getDate();
return year + fillZero(month) + fillZero(today);
}

function fillZero(str) {
return str < 10 ? '0' + str : '' + str;
}