使用 Gulp 实现前端项目哈希化压缩打包
项目概述
本文介绍如何使用 Gulp 构建工具实现前端项目的自动化打包,包括文件压缩、哈希化、混淆加密等功能,适用于需要版本控制和代码保护的 H5 项目。
项目结构 ├── js # JS 文件 │ ├── XXX.js │ ├── XXX.js │ ├── XXX.js │ └── XXX.utils.js ├── img # 图片文件 │ ├── XXX.png │ ├── XXX.png │ └── XXX.png ├── XXX.html # HTML 文件 ├── XXX.html ├── XXX.html |── XXX.html ├── dist # 构建输出目录 ├── rev # 哈希化文件清单目录 ├── gulpfile.js # Gulp 构建文件 └── package.json # 项目配置文件
依赖包安装
npm install --save-dev gulp del gulp-terser gulp-rev gulp-rev-collector gulp-imagemin javascript-obfuscator核心功能实现
导入依赖
import gulp from "gulp";
import del from "del";
import terser from "gulp-terser";
import rev from "gulp-rev";
import revCollector from "gulp-rev-collector";
import imagemin from "gulp-imagemin";
import JavaScriptObfuscator from "javascript-obfuscator";
import { Transform } from "stream";
import { execSync } from "child_process";1. 项目清理
// 1. 清理 dist rev 目录
function clean() {
return del(["dist", "rev"]);
}作用:删除之前的构建文件,避免文件冲突。
2. JavaScript 处理
// 2. 压缩并加 hash - JS
function scripts() {
return gulp
.src("js/*") // 源文件
.pipe(terser()) // 压缩JS代码
.pipe(rev()) // 生成哈希文件名
.pipe(gulp.dest("dist/js")) // 输出到dist目录
.pipe(rev.manifest("rev-manifest.json")) // 生成映射清单
.pipe(gulp.dest("rev/js")); // 保存清单文件
}关键点:
先压缩再哈希,确保哈希值基于最终文件内容
生成清单文件用于后续引用替换
构建流程顺序的重要性
// ✅ 正确顺序
.pipe(terser()) // 1. 先压缩(内容变化)
.pipe(rev()) // 2. 再哈希(基于最终内容)
// ❌ 错误顺序
.pipe(rev()) // 1. 先哈希(基于原始内容)
.pipe(terser()) // 2. 再压缩(内容变化,但哈希不变)为什么顺序重要:
哈希值应该反映最终文件内容
如果先哈希再压缩,哈希值与实际文件内容不匹配
会导致缓存破坏机制失效
3. 图片优化处理
// 3. 压缩并加 hash - 图片
function images() {
return gulp
.src("img/*", { encoding: false }) // 禁用默认编码防止二进制图片被破坏
.pipe(imagemin()) // 图片压缩优化
.pipe(rev()) // 生成哈希文件名
.pipe(gulp.dest("dist/img"))
.pipe(rev.manifest("rev-manifest.json"))
.pipe(gulp.dest("rev/img"));
}关键点:
encoding: false, 如果是二进制文件必须添加此属性,不然就会文件损坏。
默认配置下先把 PNG 当成 UTF-8 文本解码成字符串,写回时再按 UTF-8 重新编码;任何不符合 UTF-8 的原始字节都会被替换成 �,于是图片内容被破坏。把 encoding 设为 false 就是让它跳过这套文本编解码,直接保留二进制字节,因此文件恢复正常。
move 方法里面读取时也要注意。
4. HTML 文件哈希化
// 4. 哈希化 html
function html() {
return gulp
.src("./*.html") // 处理根目录HTML文件
.pipe(rev()) // HTML也进行哈希化
.pipe(gulp.dest("dist"))
.pipe(rev.manifest("rev-manifest.json"))
.pipe(gulp.dest("rev/html"));
}5. 引用路径替换
// 5. 替换 HTML 里的引用
function htmlRevise() {
return gulp
.src(["/rev/**/*.json", "dist/*.html"]) // 读取所有清单文件和目标文件(HTML)
.pipe(revCollector({ replaceReved: true })) // 替换为哈希化文件名
.pipe(gulp.dest("dist"));
}
// 6. 替换 JS 里的引用
function jsRevise() {
return gulp
.src(["/rev/**/*.json", "dist/js/*.js"]) // 读取所有清单文件和目标文件(JS)
.pipe(revCollector({ replaceReved: true }))
.pipe(gulp.dest("dist/js"));
}作用:将文件中的原始引用路径替换为带哈希的新路径。
疑问:src 的 json 文件(映射表)能不能调换位置?(可以)
代码可读性,清晰表达意图:先加载映射表,再处理目标文件
团队协作约定,大部分开源项目和文档都采用这种写法形成了事实上的标准
6. JavaScript 代码混淆加密
// 创建自定义的混淆插件
function createObfuscatorPlugin(options = {}) {
return new Transform({
objectMode: true,
transform(file, encoding, callback) {
if (file.isNull()) {
return callback(null, file);
}
try {
const obfuscatedCode = JavaScriptObfuscator.obfuscate(
file.contents.toString(),
{
compact: true, // 压缩输出
controlFlowFlattening: true, // 控制流扁平化
controlFlowFlatteningThreshold: 0.75, // 扁平化阈值
deadCodeInjection: true, // 死代码注入
deadCodeInjectionThreshold: 0.4, // 死代码注入阈值
debugProtection: true, // 调试保护
debugProtectionInterval: 4000, // 调试保护间隔
disableConsoleOutput: true, // 禁用console输出
identifierNamesGenerator: "hexadecimal", // 标识符生成方式
stringArrayEncoding: ["base64"], // 字符串数组编码
stringArrayThreshold: 0.75, // 字符串数组阈值
...options,
}
);
file.contents = Buffer.from(obfuscatedCode.getObfuscatedCode());
callback(null, file);
} catch (error) {
callback(error);
}
},
});
}
// 加密 JS
function jsEncrypt() {
const filesToEncrypt = ["dist/js/XXXXXX-*.js", "dist/js/XXXXXX-*.utils.js"];
return gulp
.src(filesToEncrypt)
.pipe(createObfuscatorPlugin()) // 应用混淆
.pipe(gulp.dest("dist/js"));
}特点:
高强度代码混淆,包含多种保护机制
只对特定文件进行混淆(埋点逻辑和工具方法)
7. 智能目录重命名
// 获取当前分支
const getPackageNamePrefix = () => {
let branch = "";
try {
branch = execSync("git rev-parse --abbrev-ref HEAD", {
encoding: "utf-8",
}).trim();
} catch (error) {
console.error("无法获取当前 git 分支,默认使用 develop 分支", error);
}
let packageNamePrefix = "";
if (branch === "master") {
packageNamePrefix = "微客";
} else if (branch === "yitongke-H5") {
packageNamePrefix = "通客";
} else {
packageNamePrefix = "Error";
}
return packageNamePrefix;
};
// 获取当前时间
function formatDate(date = new Date(), format = "YYYY-MM-DD") {
const map = {
YYYY: date.getFullYear(),
MM: ("0" + (date.getMonth() + 1)).slice(-2),
DD: ("0" + date.getDate()).slice(-2),
HH: ("0" + date.getHours()).slice(-2),
mm: ("0" + date.getMinutes()).slice(-2),
ss: ("0" + date.getSeconds()).slice(-2),
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/gi, (matched) => map[matched]);
}
// 8. 整理路径,并重命名
function move() {
return gulp
.src("dist/**", { encoding: false }) // 保持二进制资源(图片)在复制阶段不被转换
.pipe(gulp.dest(`dist/${getPackageNamePrefix()}H5-${formatDate()}`));
}关键点:
encoding: false, 如果里面有二进制文件必须添加此属性,不然就会文件损坏。
move 阶段(gulpfile.js:160)复制 dist/** 时同样保持二进制,避免已经生成的指纹资源在复制时再次被当成文本处理。
move 并不是不能读取图片,它只是做一次 “dist → dist/xxxH5-日期” 的纯复制操作,里面当然包含图片。如果把这里的 encoding:false 去掉,图片在复制过程中又会按 UTF-8 被解码/再编码,就会出现和最初一样的损坏(那些 � 字节),但与文件名里的 hash 无关——rev 只是改了文件名,不会去替换文件内容,真正破坏图片的是文本编码过程。所以保持 { encoding: false } 才能保证整条管线的图片始终以二进制形式传递。
即便只是复制,gulp.src 依然要把文件读进来,然后 gulp.dest 再把 file.contents 写出去。默认编码是 UTF-8 的话,这个“读 → 写”的过程照样会把二进制内容先解码成字符串,再重新编码成字节,最终还是复制出损坏的图片。所以对于会触碰文件内容(哪怕只是 copy)的任务,都建议显式写 { encoding: false },保证整条链路只处理 Buffer。除非你改用 read: false 让 gulp 只拿到文件路径,但那样就没法真正复制文件内容了。
功能:根据 Git 分支和日期自动生成发布目录名。
8. 构建流程定义
export const build = gulp.series(
clean, // 清理
gulp.parallel(scripts, images, html), // 并行处理静态资源
htmlRevise, // 替换HTML引用
jsRevise, // 替换JS引用
jsEncrypt, // JS混淆加密
move // 重命名输出目录
);
export default build;使用方法
# 执行完整构建
npm run build
# 或
gulp build输出结果
dist/
├── XXXH5-2025-01-15/
│ ├── index-abc123.html
│ ├── js/
│ │ ├── app-def456.js
│ │ └── utils-ghi789.js (混淆加密)
│ └── img/
│ └── logo-jkl012.png快速修改指南
修改源文件路径
// 如果源文件在 src 目录下
.src("src/js/*") // 替换 "js/*"
.src("src/img/*") // 替换 "img/*"调整混淆强度
// 降低混淆强度(提高构建速度)
const obfuscatedCode = JavaScriptObfuscator.obfuscate(
file.contents.toString(),
{
compact: true,
controlFlowFlattening: false, // 关闭控制流扁平化
deadCodeInjection: false, // 关闭死代码注入
debugProtection: false, // 关闭调试保护
}
);添加 CSS 处理
import cleanCSS from "gulp-clean-css";
function styles() {
return gulp
.src("css/*")
.pipe(cleanCSS()) // CSS压缩
.pipe(rev())
.pipe(gulp.dest("dist/css"))
.pipe(rev.manifest("rev-manifest.json"))
.pipe(gulp.dest("rev/css"));
}
// 在构建流程中添加
export const build = gulp.series(
clean,
gulp.parallel(scripts, styles, images, html), // 添加 styles
htmlRevise,
jsRevise,
jsEncrypt,
move
);插件文档地址
核心插件文档
| 插件名称 | 官方文档 | GitHub 地址 | 功能说明 |
|---|---|---|---|
| gulp | gulpjs.com | gulpjs/gulp | 构建工具核心 |
| del | npmjs.com/package/del | sindresorhus/del | 删除文件和目录 |
| gulp-terser | npmjs.com/package/gulp-terser | duan602728596/gulp-terser | JS 压缩 |
| gulp-rev | npmjs.com/package/gulp-rev | sindresorhus/gulp-rev | 文件哈希化 |
| gulp-rev-collector | npmjs.com/package/gulp-rev-collector | shonny-ua/gulp-rev-collector | 引用路径替换 |
| gulp-imagemin | npmjs.com/package/gulp-imagemin | sindresorhus/gulp-imagemin | 图片压缩 |
| javascript-obfuscator | obfuscator.io | javascript-obfuscator/javascript-obfuscator | JS 代码混淆 |
扩展插件推荐
| 插件名称 | 官方文档 | 功能说明 |
|---|---|---|
| gulp-clean-css | npmjs.com/package/gulp-clean-css | CSS 压缩 |
| gulp-sass | npmjs.com/package/gulp-sass | Sass/SCSS 编译 |
| gulp-less | npmjs.com/package/gulp-less | Less 编译 |
| gulp-autoprefixer | npmjs.com/package/gulp-autoprefixer | CSS 自动添加前缀 |
| gulp-htmlmin | npmjs.com/package/gulp-htmlmin | HTML 压缩 |
| gulp-babel | npmjs.com/package/gulp-babel | ES6+ 转换 |
| gulp-typescript | npmjs.com/package/gulp-typescript | TypeScript 编译 |
gulp.src() 详细介绍
基本语法
gulp.src(globs, [options]);参数说明
globs(必需)
文件匹配模式,可以是字符串或字符串数组:
// 单个文件
gulp.src("js/app.js");
// 通配符匹配
gulp.src("js/*.js"); // js 目录下所有 .js 文件
gulp.src("js/**/*.js"); // js 目录及子目录下所有 .js 文件
gulp.src("**/*.{js,css}"); // 所有 .js 和 .css 文件
// 数组形式(多个匹配模式)
gulp.src([
"js/**/*.js",
"css/**/*.css",
"!js/vendor/**", // 排除 vendor 目录
]);
// 排除文件(使用 ! 前缀)
gulp.src([
"src/**/*.js",
"!src/**/*.test.js", // 排除测试文件
"!src/vendor/**", // 排除第三方库
]);注意事项
构建顺序很重要:先压缩再哈希,确保哈希值正确
二进制文件的处理:读取时设置{ encoding: false },不然读取与输出过程文件编码可能会出问题
清单文件路径:注意
"/rev/**/*.json"的路径写法混淆配置:根据项目需要调整混淆强度
分支判断:根据实际 Git 分支名修改判断逻辑