返回正文
Rspack 实战,构建流程升级
在前端项目的生产构建中,我们常需要手动做三件事:更新版本号、查看构建耗时、压缩 dist 包 —— 这些重复操作不仅效率低,还容易出错(比如忘记更新版本导致线上版本混乱)。
本文就带你基于 Rspack 打造一套「自动化构建流程」:通过改造 build 脚本,实现 版本号自动递增 + 命令行日志美化 + dist 自动压缩 + 构建报告生成,最后只需一条 npm run build,就能输出带版本信息的压缩包,还能清晰看到每个环节的耗时。
一、核心功能拆解:这套脚本能解决什么问题?
先明确我们要实现的自动化目标,避免无意义的代码堆砌:
- 版本号自动管理:按「主版本。日期。每日构建次数」格式递增(如
1.20240520.3),无需手动改package.json; - 命令行日志美化:用颜色区分不同环节(清理→构建→压缩),显示精确时间和耗时,告别单调黑白色;
- dist 自动压缩:构建完成后自动打包
dist目录为ZIP包,排除sourcemap等无用文件,还能显示压缩进度; - 构建报告生成:输出总耗时、各环节耗时、版本号等信息,方便定位构建瓶颈(如压缩耗时过长)。
二、实现步骤:从依赖安装到脚本改造
第一步:安装必备依赖
首先需要安装处理压缩和命令行颜色的依赖,这里用 archiver(ZIP 压缩工具)和 chalk(命令行颜色美化):
bash
# pnpm 安装(推荐)
pnpm add archiver chalk rimraf -D
# npm 安装
npm i archiver chalk rimraf --save-dev1
2
3
4
5
2
3
4
5
- archiver:用于压缩 dist 目录为 ZIP 包,支持高压缩级别和进度跟踪;
- chalk:给命令行日志添加颜色,区分成功 / 失败 / 提示信息;
- rimraf:跨平台删除目录(替代
fs.rmdirSync,避免 Windows 下报错)。
第二步:改造 build 脚本(核心:自动化流程串联)
我们将原来的 build.js 改造成「多环节自动化」脚本,核心流程是:版本号更新 → 清理旧目录 → Rspack 构建 → dist 压缩 → 生成构建报告。
完整 build 脚本代码(build/build.js)
javascript
"use strict";
require("./check-versions")();
process.env.NODE_ENV = "production";
const ora = require("ora");
const rm = require("rimraf");
const path = require("path");
const chalk = require("chalk");
const rspack = require("@rspack/core");
const config = require("../config");
const rspackConfig = require("./rspack.prod.conf");
const fs = require("fs");
const spinner = ora("building for production...");
// 记录整个构建过程的开始时间
const totalStartTime = Date.now();
console.log(
chalk.blue(`[${new Date().toLocaleTimeString()}] 🚀 开始构建流程...`),
);
// 添加全局 navigator 修复
if (typeof global.navigator === "undefined") {
global.navigator = {
userAgent: "Node.js/" + process.version,
platform: process.platform,
language: "en-US",
};
}
spinner.start();
// 声明变量以避免作用域问题
let versionLine = "未知版本"; // 默认值
let cleanDuration = 0;
let rspackDuration = 0;
let zipDuration = 0; // 在这里声明 zipDuration
try {
function AddZero(time) {
if (time < 10) {
return "0" + time;
} else {
return time;
}
}
let packageTxt = fs.readFileSync("./package.json", "utf8");
let versionData = packageTxt.split("\n");
let packageJson = JSON.parse(packageTxt);
let VersionArr = packageJson.version.split(".");
let date = new Date();
let today =
date.getFullYear() +
"" +
AddZero(date.getMonth() + 1) +
"" +
AddZero(date.getDate());
if (today == VersionArr[1]) {
VersionArr[2] = parseInt(VersionArr[2]) + 1;
} else {
VersionArr[1] =
date.getFullYear() +
"" +
AddZero(date.getMonth() + 1) +
"" +
AddZero(date.getDate());
VersionArr[2] = 1;
}
versionLine = VersionArr.join("."); // 赋值给外部声明的变量
for (let i = 0; i < versionData.length; i++) {
if (versionData[i].indexOf('"version":') != -1) {
versionData.splice(i, 1, ' "version": "' + versionLine + '",');
break;
}
}
fs.writeFileSync("./package.json", versionData.join("\n"), "utf8");
console.log(chalk.green.bold("✅ 更新版本号成功!当前版本: " + versionLine));
} catch (e) {
console.log(chalk.red.bold("❌ 读取文件修改版本号出错:", e.toString()));
}
// 记录清理开始时间
const cleanStartTime = Date.now();
console.log(
chalk.blue(`[${new Date().toLocaleTimeString()}] 🧹 开始清理输出目录...`),
);
rm(
path.join(config.build.assetsRoot, config.build.assetsSubDirectory),
(err) => {
if (err) throw err;
// 计算清理耗时
const cleanEndTime = Date.now();
cleanDuration = (cleanEndTime - cleanStartTime) / 1000; // 赋值给外部变量
console.log(
chalk.green.bold(`✅ 清理完成! 耗时: ${cleanDuration.toFixed(2)} 秒`),
);
// 确保 navigator 可写
const originalNavigator = global.navigator;
try {
Object.defineProperty(global, "navigator", {
value: { ...global.navigator },
writable: true,
configurable: true,
});
} catch (e) {
console.warn("⚠️ 无法设置可写的 navigator:", e.message);
}
// 记录 Rspack构建开始时间 const rspackStartTime = Date.now();
console.log(
chalk.blue(`[${new Date().toLocaleTimeString()}] 📦 开始 Rspack 构建...`),
);
rspack(rspackConfig, (err, stats) => {
if (originalNavigator) {
global.navigator = originalNavigator;
}
spinner.stop();
if (err) throw err;
// 计算 Rspack 构建耗时 const rspackEndTime = Date.now();
rspackDuration = (rspackEndTime - rspackStartTime) / 1000; // 赋值给外部变量
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}) + "\n\n",
);
if (stats.hasErrors()) {
console.log(chalk.red(" ❌ 构建失败,发现错误!\n"));
process.exit(1);
}
console.log(
chalk.cyan.bold(
` ✅ Rspack 构建完成! 耗时: ${rspackDuration.toFixed(2)} 秒\n`,
),
);
console.log(
chalk.yellow(
" 💡 提示: 构建文件需要通过 HTTP 服务器提供服务\n" +
" 直接打开 index.html 可能无法正常工作\n",
),
);
try {
console.log(
chalk.blue(
`[${new Date().toLocaleTimeString()}] 🗜️ 开始打包 dist.zip...`,
),
);
const zipStartTime = Date.now();
require("./zip-dist");
const zipEndTime = Date.now();
zipDuration = (zipEndTime - zipStartTime) / 1000; // 赋值给已声明的变量
console.log(
chalk.green.bold(
`✅ dist.zip 打包完成! 耗时: ${zipDuration.toFixed(2)} 秒`,
),
);
} catch (zipErr) {
console.error(chalk.red("❌ dist.zip 打包失败:"), zipErr);
// 设置默认值以避免未定义错误
zipDuration = -1; // 使用负数表示失败
}
// 计算总耗时(秒)
const totalEndTime = Date.now();
const totalSeconds = (totalEndTime - totalStartTime) / 1000;
// 格式化总耗时为易读格式
let totalTimeText;
if (totalSeconds < 60) {
// 小于1分钟,直接显示秒
totalTimeText = `${totalSeconds.toFixed(2)} 秒`;
} else {
// 大于等于1分钟,显示分和秒
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
totalTimeText = `${minutes} 分 ${seconds.toFixed(2)} 秒`;
}
// 输出详细时间报告
console.log(
chalk.magenta.bold(
"\n==================== 构建报告 ====================",
),
);
console.log(chalk.magenta(`🔧 版本号更新: ${versionLine}`));
console.log(chalk.magenta(`🧹 清理耗时: ${cleanDuration.toFixed(2)} 秒`));
console.log(
chalk.magenta(`📦 Rspack 构建耗时: ${rspackDuration.toFixed(2)} 秒`),
);
// 安全地显示 ZIP 压缩时间
if (zipDuration >= 0) {
console.log(
chalk.magenta(`🗜️ ZIP 压缩耗时: ${zipDuration.toFixed(2)} 秒`),
);
} else {
console.log(chalk.magenta(`🗜️ ZIP 压缩: 失败`));
}
console.log(chalk.magenta.bold(`🏁 总耗时: ${totalTimeText}`));
console.log(
chalk.magenta(
`⏰ 开始时间: ${new Date(totalStartTime).toLocaleTimeString()}`,
),
);
console.log(
chalk.magenta(
`⏱️ 结束时间: ${new Date(totalEndTime).toLocaleTimeString()}`,
),
);
console.log(
chalk.magenta.bold(
"================================================\n",
),
);
// 如果有压缩错误,在此处退出
if (zipDuration < 0) {
process.exit(1);
}
});
},
);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
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
第三步:编写 dist 自动压缩脚本(build/zip-dist.js)
单独拆分 zip-dist.js 脚本,负责将 dist 目录压缩为 ZIP 包,支持进度跟踪、旧包删除、大文件过滤等功能:
ts
const fs = require("fs");
const path = require("path");
const archiver = require("archiver");
const chalk = require("chalk");
// 获取项目根目录
const rootPath = path.resolve(__dirname, "..");
const distPath = path.join(rootPath, "dist");
const packageJson = require(path.join(rootPath, "package.json"));
// 确定压缩包名称
const version = packageJson.version;
const date = new Date();
const dateStr = `${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, "0")}${date.getDate().toString().padStart(2, "0")}`;
const timeStr = `${date.getHours().toString().padStart(2, "0")}${date.getMinutes().toString().padStart(2, "0")}`;
const buildEnv = process.env.BUILD_ENV || "prod";
const zipName = process.env.ZIP_NAME || `dist.zip`;
// 这里修改了压缩包的路径,直接放到 dist 目录
const zipPath = path.join(distPath, zipName); // 原代码是 path.join(rootPath, zipName)
// 删除旧压缩包
if (fs.existsSync(zipPath)) {
try {
fs.unlinkSync(zipPath);
console.log(chalk.green(`✅ 已删除旧压缩包: ${zipName}`));
} catch (err) {
console.error(chalk.red(`❌ 删除旧压缩包失败: ${err.message}`));
}
}
// 确保 dist 目录存在
if (!fs.existsSync(distPath)) {
console.error(chalk.red(`❌ dist 目录不存在,请先运行构建命令`));
process.exit(1);
}
// 创建输出流
const output = fs.createWriteStream(zipPath);
const archive = archiver("zip", {
zlib: { level: 9 }, // 最高压缩级别
});
console.log(chalk.blue(`📦 开始创建压缩包: ${chalk.bold(zipName)}`));
console.log(chalk.blue(`🔍 源目录: ${distPath}`));
// 进度跟踪
let totalFiles = 0;
let processedFiles = 0;
let startTime = Date.now();
// 计算文件总数
function countFiles(dir) {
const files = fs.readdirSync(dir);
let count = 0;
files.forEach((file) => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
count += countFiles(filePath);
} else {
count++;
}
});
return count;
}
// 更新进度显示
function updateProgress() {
if (totalFiles === 0) return;
const percent = Math.round((processedFiles / totalFiles) * 100);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
process.stdout.write(
`\r📦 压缩进度: ${percent}% (${processedFiles}/${totalFiles} 文件) [${elapsed}s]`,
);
}
// 监听事件
output.on("close", () => {
const sizeMB = (archive.pointer() / 1024 / 1024).toFixed(2);
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(chalk.green.bold(`✅ 压缩包创建成功`));
console.log(chalk.gray(` ├─ 大小: ${chalk.cyan(`${sizeMB} MB`)}`));
console.log(chalk.gray(` └─ 路径: ${chalk.white(zipPath)}\n`));
});
archive.on("error", (err) => {
console.error(chalk.red(`\n❌ 压缩失败: ${err.message}`));
process.exit(1);
});
archive.on("warning", (err) => {
if (err.code === "ENOENT") {
console.warn(chalk.yellow(`⚠️ 文件警告: ${err.message}`));
} else {
console.error(chalk.red(`\n❌ 压缩警告: ${err.message}`));
process.exit(1);
}
});
archive.on("entry", (entry) => {
if (entry.name) {
processedFiles++;
// 每处理50个文件或完成时更新进度
if (processedFiles % 50 === 0 || processedFiles === totalFiles) {
updateProgress();
}
}
});
// 管道连接
archive.pipe(output);
// 递归添加目录中的所有文件
function addDirectory(dir, parentDir = "") {
const files = fs.readdirSync(dir);
files.forEach((file) => {
const filePath = path.join(dir, file);
const relativePath = parentDir ? path.join(parentDir, file) : file;
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
addDirectory(filePath, relativePath);
} else {
// 排除 sourcemap 文件
if (filePath.endsWith(".map")) return;
// 排除大文件(可选)
if (stat.size > 30 * 1024 * 1024) {
console.warn(
chalk.yellow(
`⚠️ 跳过大文件: ${relativePath} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`,
),
);
return;
}
archive.file(filePath, { name: relativePath });
}
});
}
// 添加 dist 目录中的所有文件
try {
// 计算文件总数
totalFiles = countFiles(distPath);
console.log(chalk.blue(`📂 发现 ${totalFiles} 个文件待压缩`));
// 添加 dist 目录
addDirectory(distPath);
// 完成压缩
archive.finalize();
} catch (err) {
console.error(chalk.red(`❌ 压缩准备失败: ${err.message}`));
process.exit(1);
}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
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
V 0.11.19 |
基于 MIT Licensed版权所有 © 2009- 2026 CMONO.NET
本站访客数
--次 本站总访问量
--人次 