SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

使用 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 文件(映射表)能不能调换位置?(可以)

  1. 代码可读性,清晰表达意图:先加载映射表,再处理目标文件

  2. 团队协作约定,大部分开源项目和文档都采用这种写法形成了事实上的标准

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 地址功能说明
gulpgulpjs.comgulpjs/gulp构建工具核心
delnpmjs.com/package/delsindresorhus/del删除文件和目录
gulp-tersernpmjs.com/package/gulp-terserduan602728596/gulp-terserJS 压缩
gulp-revnpmjs.com/package/gulp-revsindresorhus/gulp-rev文件哈希化
gulp-rev-collectornpmjs.com/package/gulp-rev-collectorshonny-ua/gulp-rev-collector引用路径替换
gulp-imageminnpmjs.com/package/gulp-imageminsindresorhus/gulp-imagemin图片压缩
javascript-obfuscatorobfuscator.iojavascript-obfuscator/javascript-obfuscatorJS 代码混淆

扩展插件推荐

插件名称官方文档功能说明
gulp-clean-cssnpmjs.com/package/gulp-clean-cssCSS 压缩
gulp-sassnpmjs.com/package/gulp-sassSass/SCSS 编译
gulp-lessnpmjs.com/package/gulp-lessLess 编译
gulp-autoprefixernpmjs.com/package/gulp-autoprefixerCSS 自动添加前缀
gulp-htmlminnpmjs.com/package/gulp-htmlminHTML 压缩
gulp-babelnpmjs.com/package/gulp-babelES6+ 转换
gulp-typescriptnpmjs.com/package/gulp-typescriptTypeScript 编译

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/**", // 排除第三方库
]);

注意事项

  1. 构建顺序很重要:先压缩再哈希,确保哈希值正确

  2. 二进制文件的处理:读取时设置{ encoding: false },不然读取与输出过程文件编码可能会出问题

  3. 清单文件路径:注意 "/rev/**/*.json" 的路径写法

  4. 混淆配置:根据项目需要调整混淆强度

  5. 分支判断:根据实际 Git 分支名修改判断逻辑

最后更新时间:
贡献者: 何风顺
上一页
信息泄露
下一页
前端缓存策略与文件哈希化的关联解析