# 技术方案
- 通过
docker
运行一个nginx
容器,并挂载宿主机目录; - 前端编写
deploy.js
上传脚本,将静态资源上传到与该nginx
容器关联的宿主机目录中
# 安装 Nginx
# 基于 Docker 安装 Nginx
# 安装命令
docker pull nginx
1
# 启动一个简单的 Nginx 镜像
# 是否成功安装 Nginx 镜像
# 运行一个 nginx 容器
docker run -d -p 80:80 --name nginx nginx
1
# 修改 Nginx 配置文件
根据场景有以下两种方式,进入容器直接修改 或者 将镜像内的配置文件与宿主机的文件目录做一个映射
# 进入容器内部直接修改
只适用于修改频率不高的场景,每次修改配置需进入容器内部
# 查看容器 id 并进入容器内部
docker ps
查看容器列表
docker exec -it 容器id /bin/bash
1
exit
退出容器
# Nginx 容器内部大致目录结构
/etc/nginx
: 配置文件目录
/var/log/nginx
: 日志目录
/usr/share/nginx/html
:默认前端代码目录
# 配置文件映射到宿主机文件
# 创建宿主机挂载目录
- 回到服务器根目录
cd ~
1
- 创建文件
mkdir -p home/nginx/{html,log,conf}
1
- html: 存放前端 html 页面
- log: 存放 Nginx log,后期可以查看 access 和 error 日志
- conf: 存放 nginx 配置
# 将容器内配置文件拷贝到宿主
根目录下执行
docker cp 容器id:/etc/nginx/nginx.conf ./home/nginx/conf/nginx.conf
docker cp 容器id:/etc/nginx/conf.d ./home/nginx/conf/conf.d
docker cp 容器id:/usr/share/nginx/html ./home/nginx
1
2
3
2
3
# 关闭并删除之前启动的 Nginx 容器
docker stop 容器id
docker rm -f 容器id
1
2
2
# 启动新的 Nginx 容器
使用 --mount
命令挂载宿主机的文件和目录作为数据卷
docker run -d \
-p 80:80 \
--name nginx \
--mount type=bind,source=$HOME/home/nginx/conf/nginx.conf,target=/etc/nginx/nginx.conf \
--mount type=bind,source=$HOME/home/nginx/conf/conf.d,target=/etc/nginx/conf.d \
--mount type=bind,source=$HOME/home/nginx/log,target=/var/log/nginx \
--mount type=bind,source=$HOME/home/nginx/html,target=/usr/share/nginx/html \
nginx
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 测试是否挂载成功
- 更改 home/nginx/html/index.html 页面
docker restart 容器id
# 上传前端静态资源
上一步我们挂载了宿主机目录/文件作为 Nginx
容器的数据卷;
接下来我们将 Vue
或者 React
打包生成的 dist
或 build
上传到 home/nginx/html
中,并简单配置一下 Nginx
,我们的前端部署就算是完成了
# 创建一个 Vue3 项目
pnpm create vite vue3-app --template vue-ts
cd vue3-app
pnpm install
pnpm run dev
1
2
3
4
5
6
7
2
3
4
5
6
7
# 编写部署脚本
# 大致流程
# 需要用的三方库
npm i node-ssh compressing dotenv inquirer ora --save-dev
1
compressing
:文件压缩inquirer
:命令行交互工具dotEnv
:获取在.env
文件中定义的环境变量node-ssh
:ssh
工具,登录远程服务器ora
:执行npm run build
时,在命令行中添加loading
效果
# deploy.js
import fs from "node:fs/promises";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import childProcess from "node:child_process";
import ora from "ora";
import { NodeSSH } from "node-ssh";
import compressing from "compressing";
import dotEnv from "dotenv";
import inquirer from "inquirer";
const BUILD_SCRIPT = "npm run build";
const deploy = async () => {
// 需要创建 .env 文件,并在 .env 文件中定义服务器 host 地址
const { HOST } = process.env;
if (!HOST) {
console.log("🤔🤔部署失败,环境变量无法加载");
process.exit(0);
}
const config = {
host: HOST,
deployType: "deploy",
};
inquirer
.prompt([
{
type: "input",
message: "😄😄请输入服务器用户名",
name: "username",
},
{
type: "password",
message: "🫣 🫣 请输入服务器密码",
name: "password",
when: (answers) => answers.username,
},
{
type: "list",
message: "请选择部署方式",
name: "deployType",
choices: [
{ value: "deploy", name: "部署新版" },
{ value: "reset", name: "回退版本" },
],
when: (answers) => answers.username && answers.password,
},
])
.then(({ username, password, deployType }) => {
config.username = username;
config.password = password;
config.deployType = deployType;
// 连接服务器
connectServer(config);
})
.catch((err) => {
console.log(err);
});
};
const connectServer = async (config) => {
try {
const ssh = new NodeSSH();
await ssh.connect({
host: config.host,
username: config.username,
password: config.password,
tryKeyboard: true,
});
console.log("🚀 服务器连接成功");
if (config.deployType === "deploy") {
handleUpload(ssh);
}
if (config.deployType === "reset") {
handleReset(ssh);
}
} catch (error) {
console.log("❌ 服务器连接失败,请检查服务器配置🤔🤔");
process.exit(0);
}
};
const handleReset = async (ssh) => {
const sftp = await ssh.requestSFTP();
sftp.exists("/root/home/nginx/html/dist-last", async (boolean) => {
if (boolean) {
await ssh.execCommand("cd home/nginx/html && rm -rf dist");
await ssh.execCommand("cd home/nginx/html && mv dist-last dist");
await ssh.execCommand("docker restart nginx");
console.log("🚀 docker nginx 镜像开始重启");
console.log("🚀🚀🚀 回退成功 🚀🚀🚀");
process.exit(0);
} else {
console.log("❌ 回滚失败,未发现备份产物");
process.exit(0);
}
});
};
const handleZipFile = () => {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
await compressing.tar.compressDir("dist", "dist.tar");
await compressing.gzip.compressFile("dist.tar", "dist.tgz");
resolve(true);
} catch (err) {
console.log("❌ 文件压缩失败", err);
reject(false);
process.exit(0);
}
});
};
const handleRunBuild = () => {
const spinner = ora("开始构建 build 产物").start();
try {
childProcess.execSync(BUILD_SCRIPT);
spinner.stop();
console.log("🚀 build产物构建成功, 开始压缩文件");
} catch (error) {
console.log("❌ npm run build 执行失败");
spinner.stop();
process.exit(0);
}
};
const handleUpload = async (ssh) => {
handleRunBuild();
const isSuccess = await handleZipFile();
if (isSuccess) {
console.log("🚀 文件压缩成功,开始上传文件");
const __filename = fileURLToPath(import.meta.url);
const distPath = `${dirname(__filename)}/dist.tgz`;
try {
await ssh.putFile(distPath, "/root/home/nginx/html/dist.tgz");
console.log("🚀 上传文件成功");
} catch (error) {
console.log("❌ 上传文件失败", error);
process.exit(0);
}
const sftp = await ssh.requestSFTP();
sftp.exists("/root/home/nginx/html/dist", async (boolean) => {
if (boolean) {
await ssh.execCommand(
"cd home/nginx/html && rm -rf dist-list && cp -r dist dist-last"
);
console.log("🚀 构建产物备份成功");
} else {
console.log("⚠️ 未发现上次构建产物");
}
});
try {
await ssh.execCommand("cd home/nginx/html && tar -zxvf dist.tgz -C ./");
console.log("🚀 服务器解压文件成功");
await ssh.execCommand("docker restart nginx");
console.log("🚀 docker nginx 镜像开始重启");
await ssh.execCommand("cd home/nginx/html && rm -rf dist.tgz");
console.log("🚀 服务器压缩文件已删除");
await removeLocalFile();
console.log("🚀🚀🚀 部署成功 🚀🚀🚀");
process.exit(0);
} catch (error) {
console.log("❌ 部署失败,请检查服务器", error);
process.exit(0);
}
}
};
const removeLocalFile = async () => {
const __filename = fileURLToPath(import.meta.url);
try {
await fs.unlink(`${dirname(__filename)}/dist.tar`);
await fs.unlink(`${dirname(__filename)}/dist.tgz`);
console.log("🚀本地压缩文件已删除");
} catch (error) {
console.log("❌本地压缩文件删除失败", error);
}
};
// 获取环境变量
dotEnv.config();
// 执行部署代码;
deploy();
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
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
# 添加 package.json script 脚本
"script": {
...忽略其它
"deploy": "node deploy.js"
}
1
2
3
4
2
3
4
# 效果图
# 配置 Nginx
# 修改 html 文件路径
- 打开 Nginx 配置文件
vim home/nginx/conf/conf.d/default.conf
1
- 修改 root 路径
- 重启 nginx 容器
docker restart nginx
1
- 查看结果
# 配置静态资源缓存
# 缓存策略
带 hash 的资源,配置 Cache-Control: max-age=31536000
不带 hash 的资源,配置 Cache-Control: no-cache
# 配置域名
前提是你有一个域名,并且已经成功备案
# 配置 DNS 解析 (opens new window)(阿里云为例)
配置完成后,静等几分钟就会生效
# 修改 nginx 配置
修改 default.conf
文件将 server_name
修改成域名地址即可
修改完成后,docker restart nginx
重启 nginx
容器
# 开启 gzip
配置文件地址: nginx/config/conf/nginx.conf
http {
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10