使用 grunt 生成组件级别缓存键并注入 HTML

解读

在国内前端工程化面试中,“组件级缓存键”通常指为每个可独立发布的业务组件(如 header、banner、商品卡片)生成带内容哈希的文件名(如 card.3e2f1a.js),并在最终 HTML 中自动替换引用路径,实现**“发版即刷新缓存、未改动即复用缓存”的极致性能策略。
Grunt 作为老牌任务运行器,本身
不内置 webpack 式的 chunk 拆分**,但借助 grunt-hash、grunt-filerev、grunt-usemin、grunt-processhtml 等插件组合,可完全覆盖“哈希→映射→注入”三步流程。面试官重点考察:

  1. 能否在 Grunt 生态里闭环解决“文件名带哈希”与“HTML 自动替换”两大痛点
  2. 是否理解组件级粒度(非整包)与多入口场景(如 SSR、微前端、多页应用)下的缓存策略差异;
  3. 映射文件持久化、CDN 路径拼接、SourceMap 同步、回滚方案等生产细节是否有落地经验。

知识点

  1. grunt-filerev:基于文件内容生成 MD5 戳并重命名,输出 filerev.summary 映射表。
  2. grunt-usemingrunt-processhtml:在 HTML 里标记 <!-- build:js scripts/card.js --> 区块,自动把映射后的哈希文件名写回。
  3. 组件级入口:通过 expand:true 动态遍历 components/*/index.js,保证每个组件独立生成哈希。
  4. 映射持久化:把 filerev.summary 写入 rev-manifest.json,供后端模板或 Node 中间件二次读取,实现非覆盖式灰度发布
  5. 缓存控制:配合 grunt-cachebuster 在查询串加哈希,解决部分 CDN 对“同名文件”缓存时间过长的合规问题(国内阿里云、腾讯云默认 10 分钟)。
  6. 并行优化:使用 grunt-concurrentfilerev、uglify、imagemin 并行,降低大项目 40% 构建耗时,满足国内“下班前必须出包”的强节奏。
  7. 回滚策略:在 filerev 后追加 grunt-copy 保留上一版本文件,结合 grunt-ssh-deploy 一键回滚,避免“哈希式发布”后找不到旧文件导致 404。

答案

  1. 安装核心插件
npm i -D grunt-filerev grunt-usemin grunt-contrib-copy grunt-contrib-clean
  1. 目录约定(符合国内团队习惯)
├─ components/
│  ├─ card/
│  │  ├─ index.js
│  │  └─ index.less
│  └─ search/
│     ├─ index.js
│     └─ index.less
├─ dist/          # 构建产出
├─ rev/           # 映射表
└─ Gruntfile.js
  1. Gruntfile.js 关键配置
module.exports = function(grunt) {
  grunt.initConfig({
    clean: ['dist', 'rev'],

    // 1. 先合并、压缩每个组件
    uglify: {
      components: {
        files: [{
          expand: true,
          cwd: 'components',
          src: '*/index.js',
          dest: 'dist/js',
          rename: function(dest, src) {
            // 把 card/index.js 变成 dest/js/card.js
            return dest + '/' + src.replace(/\/index\.js$/, '.js');
          }
        }]
      }
    },

    // 2. 生成带哈希的文件名
    filerev: {
      options: {
        algorithm: 'md5',
        length: 8
      },
      comp: { src: 'dist/js/*.js' }
    },

    // 3. 把哈希结果写回 HTML
    usemin: {
      html: 'src/*.html',
      options: {
        assetsDirs: ['dist'],
        // 读取 filerev 生成的映射
        revmap: 'rev/filerev.json'
      }
    },

    // 4. 持久化映射,供后端使用
    filerev_assets: {
      dist: {
        options: {
          dest: 'rev/filerev.json',
          pretty: true
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-filerev');
  grunt.loadNpmTasks('grunt-usemin');
  grunt.loadNpmTasks('grunt-contrib-clean');

  grunt.registerTask('default', [
    'clean',
    'uglify',
    'filerev',
    'filerev_assets',
    'usemin'
  ]);
};
  1. HTML 模板示例(src/index.html)
<!-- build:js js/card.js -->
<script src="components/card/index.js"></script>
<!-- endbuild -->
  1. 构建后效果
  • 磁盘文件:dist/js/card.3e2f1a8c.js
  • HTML 输出:<script src="js/card.3e2f1a8c.js"></script>
  • 映射文件:rev/filerev.json 内含 {"js/card.js":"js/card.3e2f1a8c.js"},供后端 Java、Node 模板引擎动态读取,实现**“组件级缓存键”**闭环。

拓展思考

  1. 微前端场景:若使用 qiankun、micro-app,每个子应用独立仓库,Grunt 可在 CI 阶段把 rev-manifest.json 推送到统一版本中心,主应用通过 HTTP 拉取最新哈希,实现**“子应用热更新而主应用无感”**。
  2. 多环境路径差异:国内生产环境普遍走 CDN,测试环境走本地 Nginx,需在 usemin 前加 grunt-replacesrc="/js/ 替换为 src="https://cdn.example.com/js/,避免硬编码域名。
  3. SourceMap 同步filerev 后记得把 *.map 文件也同步重命名,否则 Sentry 上报无法还原行列号;可写自定义 task 读取 filerev.summary 批量重命名 map。
  4. 灰度回滚:在 filerev 后追加 grunt-ssh-deploy,把旧版本文件保留为 card.3e2f1a8c.js.bak,一旦监控告警,可在 30 秒内执行 mv *.bak *.js 回滚,满足国内“晚间上线、凌晨回滚”的严苛 SLA。