外观
部署上线
获取服务器的SSH账号信息
配置好 .env 或 .env.* 中的 SSH_* 相关的服务器配置和 QINIU_ 相关的七牛云 OSS 配置。
部署命令
部署非常简单。只要执行下面的命令(以部署到 production 环境为例)就可以编译并部署到服务器和七牛云OSS云存储服务上了。
全量部署:
bash
npm run deploy1
支持 TASKS 参数
该命令支持通过 TASKS 来指定具体执行哪些任务(不传的话默认执行全部任务)。
该参数支持白名单和黑名单模式,并且支持 * 通配符。注意,白名单和黑名单模式不可混用,以避免歧义。
bash
# 白名单:执行所有 spa: 开头的任务
cross-env TASKS=spa:* npm run <example-command>
# 白名单:混合精确匹配和通配符
cross-env TASKS=cleanAllBuildFiles,ssg:* npm run <example-command>
# 黑名单:排除所有 spa: 开头的任务
cross-env TASKS=-spa:* npm run <example-command>1
2
3
4
5
6
2
3
4
5
6
scripts/deploy.mts 脚本的内容
typescript
import { isPathExists, joinPath, joinPosixPath, logInfo } from "nsuite";
import { useTask } from "#scripts/utils/use-task";
import {
APP_NAME,
CDN_PREFIX,
MODE,
NODE_ENV,
PATH_ASSETS,
PATH_CONSUMERS,
PATH_PATCHES,
PATH_PUBLIC,
PATH_ROOT,
PATH_SCHEDULES,
PATH_SRC,
PATH_TYPINGS,
PATH_VIEWS,
QINIU_BUCKET_NAME,
RAW_HOME_URL,
SERVER_PORT,
STANDALONE_APPS_NAMES,
SUB_APPS_NAMES,
} from "#utils/ConstantUtils";
import { cleanAllBuildFiles } from "#scripts/utils/clean-files";
import { readdir } from "fs/promises";
import { runCommandInChildProcess } from "#scripts/utils/child-process";
import { getAllSourceFilesInPublic } from "#scripts/utils/public-builder";
import { useOSS } from "#scripts/utils/use-oss";
import { basename, extname } from "node:path";
import { useSSH } from "#scripts/utils/use-ssh";
import { sshConfig } from "#hosts/Shanghai/nginx/scripts/config";
import { logger } from "#utils/LogUtils";
const { killAllChildProcessWhenSuitable, addTask, runTasksSequentially, printTaskStatisticsInfo } =
useTask({ allowTasks: process.env.TASKS || "" });
killAllChildProcessWhenSuitable();
addTask("public:static", async () => {
// 遍历public下的子目录,将其中除了css、js、static之外的目录(不含文件)都排除掉
const files = await readdir(PATH_PUBLIC, { withFileTypes: true });
const ignorePathList: string[] = ["node_modules/"];
const includesArr: string[] = ["css", "js", "static"];
for (const file of files) {
if (file.isDirectory() && !includesArr.includes(file.name)) {
ignorePathList.push(joinPath(PATH_PUBLIC, file.name, "**"));
}
}
const [jsSourceFiles, cssSourceFiles] = await getAllSourceFilesInPublic();
ignorePathList.push(...jsSourceFiles, ...cssSourceFiles);
const { uploadLocalPath, refreshUrls } = useOSS({
bucketName: QINIU_BUCKET_NAME,
cdnOrigin: new URL(CDN_PREFIX).origin,
});
const uploadedList = await uploadLocalPath({
localPath: PATH_PUBLIC,
ignorePathList,
remotePath: new URL(CDN_PREFIX).pathname.replace(/^\//g, ""),
});
const urlsToRefresh = uploadedList
.filter((item) => {
if (item.key.includes("fontawesome") || item.key.includes("bootstrap")) {
return false;
}
return item.key.endsWith(".css") || item.key.endsWith(".js");
})
.map((item) => item.url);
logInfo(`Start refreshing CDN: ${urlsToRefresh.join(", ")}.`);
const refreshedUrls = await refreshUrls({
urls: urlsToRefresh,
});
logInfo(`refreshedUrls: ${refreshedUrls.join(", ")}.`);
});
for (const appName of SUB_APPS_NAMES) {
addTask(`spa:${appName}`, async (payload) => {
const { progress, taskName } = payload;
const { promise } = runCommandInChildProcess({
appName: `${progress.current}/${progress.total}-${taskName}`,
command: "node",
args: `./apps/${appName}/scripts/deploy.mts`,
env: process.env,
});
const exitCode = await promise;
if (exitCode) {
throw new Error(`Task ${taskName} failed: exitCode=${exitCode}`);
}
});
}
for (const appName of STANDALONE_APPS_NAMES) {
addTask(`ssg:${appName}`, async (payload) => {
const { progress, taskName } = payload;
const { promise } = runCommandInChildProcess({
appName: `${progress.current}/${progress.total}-${taskName}`,
command: "node",
args: `./apps-home/${appName}/scripts/deploy.mts`,
env: process.env,
});
const exitCode = await promise;
if (exitCode) {
throw new Error(`Task ${taskName} failed: exitCode=${exitCode}`);
}
});
}
// cwd, e.g.: `/www/sites/www.example.com`
const cwd = `/www/sites/${new URL(RAW_HOME_URL).hostname}`;
const { uploadLocalPath, uploadFile, execCommands } = useSSH({ sshConfig });
/**
* 部署无需特殊处理可以知己上传到服务器上的目录
*/
const pathListDirectlyUpload: string[] = [
PATH_ASSETS,
PATH_CONSUMERS,
PATH_PATCHES,
PATH_SCHEDULES,
PATH_SRC,
PATH_TYPINGS,
PATH_VIEWS,
];
for (const pathUpload of pathListDirectlyUpload) {
const folderName = basename(pathUpload);
addTask(`server:upload:${folderName}`, async () => {
await uploadLocalPath({
localPath: pathUpload,
remotePath: joinPosixPath(cwd, folderName),
});
});
}
/**
* 项目根目录下的public文件夹需要特殊处理,只上传其下紧跟着的文件,其中 robots-*.txt 需要改名成 robots.txt 再上传
*/
addTask("server:upload:public:direct", async () => {
const files = await readdir(PATH_PUBLIC, { withFileTypes: true });
for (const file of files) {
if (file.isFile() && !file.name.startsWith("robots-")) {
const localFile = joinPath(PATH_ROOT, "public", file.name);
const remoteFile = joinPosixPath(cwd, "public", file.name);
await uploadFile({
localFile,
remoteFile,
});
logger.warn(`Uploaded: ${localFile} => ${remoteFile}`);
}
}
await uploadFile({
localFile: joinPath(PATH_ROOT, "public", `robots-${MODE}.txt`),
remoteFile: joinPosixPath(cwd, "public", "robots.txt"),
});
});
/**
* 项目根目录下的public文件夹需要特殊处理,上传 css、js 目录下的编译产物
*/
addTask("server:upload:public:built", async () => {
const ignorePathList: string[] = ["node_modules/"];
const includesArr: string[] = ["css", "js"];
const files = await readdir(PATH_PUBLIC, { withFileTypes: true });
for (const file of files) {
if (file.isFile()) {
ignorePathList.push(joinPath(PATH_PUBLIC, file.name));
}
if (file.isDirectory() && !includesArr.includes(file.name)) {
ignorePathList.push(joinPath(PATH_PUBLIC, file.name, "**"));
}
}
const [jsSourceFiles, cssSourceFiles] = await getAllSourceFilesInPublic();
ignorePathList.push(...jsSourceFiles, ...cssSourceFiles);
const { uploadLocalPath, refreshUrls } = useOSS({
bucketName: QINIU_BUCKET_NAME,
cdnOrigin: new URL(CDN_PREFIX).origin,
});
const uploadedList = await uploadLocalPath({
localPath: PATH_PUBLIC,
ignorePathList,
remotePath: new URL(CDN_PREFIX).pathname.replace(/^\//g, ""),
});
const urlsToRefresh = uploadedList
.filter((item) => {
return item.key.endsWith(".min.css") || item.key.endsWith(".min.js");
})
.map((item) => item.url);
logInfo(`Start refreshing CDN: ${urlsToRefresh.join(", ")}.`);
const refreshedUrls = await refreshUrls({
urls: urlsToRefresh,
});
logInfo(`refreshedUrls: ${refreshedUrls.join(", ")}.`);
});
/**
* 上传根目录下的子文件
*/
addTask("server:upload:root", async () => {
const filenames = [
".editorconfig",
".env",
`.env.${MODE}`,
".npmrc",
".nvmrc",
"knexfile.mts",
"package.json",
"package-lock.json",
"tsconfig.json",
];
for (const filename of filenames) {
const localFile = joinPath(PATH_ROOT, filename);
if (!(await isPathExists(localFile))) {
throw new Error(`file not exists for ${localFile}`);
}
const remoteFile = joinPosixPath(cwd, filename);
await uploadFile({
localFile,
remoteFile,
});
logger.warn(`Uploaded: ${localFile} => ${remoteFile}`);
}
});
const nodePath = "/root/.nvm/versions/node/v22.18.0/bin";
const npm = `${nodePath}/npm`;
const commandsSetupEnv: string[] = [
`export PATH=$PATH:${nodePath}`,
`export NODE_ENV=${NODE_ENV}`,
`export MODE=${MODE}`,
"source ~/.nvm/nvm.sh 2>/dev/null || true",
"nvm use v22.18.0 2>/dev/null || true",
];
addTask("server:dependencies", async () => {
await execCommands(cwd, [...commandsSetupEnv, `${npm} install`]);
});
const pm2 = `${nodePath}/pm2`;
const appName = `${APP_NAME}-${SERVER_PORT}`;
addTask("server:main", async () => {
await execCommands(cwd, [
...commandsSetupEnv,
`${pm2} stop ${appName} || true`,
`${pm2} start ./src/bin/www.mts --name ${appName} --max-memory-restart 300M --log-date-format "YYYY-MM-DD HH:mm:ss.SSS"`,
]);
});
// 启动消费者进程(仅生产环境)
const files = await readdir(PATH_CONSUMERS, { withFileTypes: true });
for (const file of files) {
if (file.isFile() && file.name.endsWith(".mts")) {
const filename = file.name;
const baseFilename = basename(filename, extname(filename));
addTask(`server:consumers:${filename}`, async () => {
await execCommands(cwd, [
...commandsSetupEnv,
`${pm2} stop ${baseFilename} || true`,
`${pm2} start ./consumers/${filename} --name ${baseFilename} --max-memory-restart 300M --log-date-format "YYYY-MM-DD HH:mm:ss.SSS"`,
]);
});
}
}
addTask("clean", async () => {
await cleanAllBuildFiles();
});
await runTasksSequentially();
printTaskStatisticsInfo();
logger.info(
`Finished deployment, you can run command "npm run watch:server" to see server-side logs.`,
);
process.exit(0);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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
提示
部署时,一部分文件是部署到服务器上的,还有一部分文件会被部署到七牛云OSS上以加快静态资源的访问速度。
对于部署到服务器上的文件夹,我们会自动将其打包成 zip 传输到服务器后再进行解压,以加快部署速度,并减少文件上传失败的概率。