使用 grunt-filerev 为静态资源添加 MD5 哈希并生成映射表

解读

在国内前端工程化面试中,资源缓存与版本管理是高频考点。面试官抛出“用 grunt-filerev 加 MD5 哈希”这一题,表面看是插件使用,实则想验证候选人是否理解:

  1. 缓存击穿与版本号策略(为什么用哈希而非时间戳)
  2. Grunt 插件链式协作(filerev 与 usemin、cdnify、uglify 如何配合)
  3. 映射表落地方式(如何被后端模板或 Node 中间件消费)

若只答“装插件、配任务”,会被追问“映射表怎么用”“CSS 中 url() 如何自动替换”,因此答案必须覆盖配置、替换、消费三环节,并给出国内主流场景的落地细节。

知识点

  1. grunt-filerev 核心机制
    对指定文件计算 MD5 并改写文件名,生成 dest/filerev.json 映射表,格式为 { "原始路径": "带哈希路径" }

  2. Grunt 任务阶段与文件对象流
    filerev 属于 “编译后处理” 阶段,需保证在 uglify、imagemin 之后,useminPrepare/usemin 之前,否则哈希会重复或路径不一致。

  3. 映射表消费方案
    国内常见三种:

    • Node 中间件:Egg、Koa 在静态资源服务中间件里读映射表,模板引擎直接用 helper.asset('a.js') 输出带哈希路径。
    • JSP/Velocity 方案:Gradle 或 Jenkins 构建时把映射表注入到 global.jsMap,前端用 ${jsMap.get("a.js")} 拼链接。
    • 前端自动替换:grunt-usemin 读取映射表,一次性把 HTML/CSS/JS 中的引用全部改写,适合纯静态站点托管在阿里云 OSS+CDN 场景。
  4. 路径对齐与 publicPath
    若项目采用“源码—中间产物—发布目录”三级结构(src/.tmp/dist),必须在 filerev 中设置 options: { basePath: 'dist/', algorithm: 'md5', length: 8 },保证映射表 key 不带多余前缀,否则后端拼接域名会 404。

  5. 并发与增量
    大型项目(如电商活动页 2000+ 图片)需开启 grunt-newerfilerevoptions: { algorithm: 'md5', length: 6 } 降低 CPU 占用,同时把映射表缓存到 .grunt/grunt-filerev-cache.json,实现增量构建,符合国内“下班前必须出包”的强节奏。

答案

  1. 安装与版本锁定

    npm i -D grunt-filerev@2.3.1   # 锁定版本,避免 3.x 的 API 差异
    
  2. Gruntfile 配置(可直接用于生产)

    module.exports = function(grunt) {
      grunt.initConfig({
        // 1. 先清理
        clean: {
          dist: 'dist/**/*'
        },
        // 2. 复制并压缩
        copy: {
          assets: {
            files: [{
              expand: true,
              cwd: 'src/',
              src: ['**/*.{png,jpg,js,css}'],
              dest: 'dist/'
            }]
          }
        },
        // 3. 加 MD5
        filerev: {
          options: {
            algorithm: 'md5',
            length: 8,
            basePath: 'dist/',          // 关键:去掉 dist 前缀
            process: function(basename, name, ext) {
              // 保留原始目录结构,避免同名冲突
              return basename + '.' + name.split('/').join('_');
            }
          },
          assets: {
            src: [
              'dist/**/*.{js,css,png,jpg}',
              '!dist/**/vendor/*'        // 第三方库不哈希,方便公共缓存
            ]
          }
        },
        // 4. 生成映射表后,自动替换
        usemin: {
          html: 'dist/**/*.html',
          css:  'dist/**/*.css',
          options: {
            assetsDirs: ['dist'],
            patterns: {
              js: [[/["']([^"']+\.js)["']/g, '替换 JS 引用']],
              css:[[/url\(\s*['"]?([^"')]+)['"]?\s*\)/g, '替换 CSS 背景图']]
            }
          }
        },
        // 5. 把映射表输出给后端
        filerev_assets: {
          dist: {
            options: {
              dest: 'dist/asset-map.json',   // 后端可读取
              cwd: 'dist/'
            }
          }
        }
      });
    
      grunt.loadNpmTasks('grunt-contrib-clean');
      grunt.loadNpmTasks('grunt-contrib-copy');
      grunt.loadNpmTasks('grunt-filerev');
      grunt.loadNpmTasks('grunt-usemin');
      grunt.loadNpmTasks('grunt-filerev-assets'); // 社区插件,用于导出映射表
    
      grunt.registerTask('build', [
        'clean:dist',
        'copy:assets',
        'filerev',
        'usemin',
        'filerev_assets'
      ]);
    };
    
  3. 后端消费示例(Egg 框架)

    // app/service/asset.js
    const map = require('../../dist/asset-map.json');
    exports.url = function(origin) {
      return map[origin] || origin;
    };
    // 模板里
    // <script src="<%= helper.asset('js/app.js') %>"></script>
    // 输出 <script src="/js/app.3f4e5a6b.js"></script>
    
  4. 上线验证
    在 Jenkins 流水线中加一步 grep -q 'app.[a-f0-9]{8}.js' dist/index.html || exit 1,确保哈希真正被注入,避免“构建成功但缓存未击穿”的线上事故。

拓展思考

  1. 若团队已迁移到 webpack,是否还需要 grunt-filerev?
    国内很多存量项目(如银行、运营商后台)仍用 Grunt 流水线,直接重写风险高。可采用**“双轨构建”:新业务用 webpack 5 模块联邦,老业务继续 grunt-filerev,通过共享映射表**让同一页面混合引用两种资源,实现渐进式迁移。

  2. 如何支持“雪碧图”或“字体图标”自动哈希?
    grunt-filerev 默认不处理 ?v=hash 查询串,需手写 grunt-string-replace 任务,把 url(icon.woff?v=#rev#) 替换成 url(icon.3f4e5a6b.woff),否则 HTTP2 环境下字体缓存仍按旧名失效。

  3. 灰度发布与回滚
    国内大厂常把映射表上传到 Redis,灰度阶段只让 10% 机器读新映射表。回滚时只需把 Redis key 指回旧表,无需重新打包,保证“秒级回滚”能力,这也是面试官想听到的工程化深度

  4. 性能极限优化
    对于 10W+ 小图标的活动会场,可把 filerev 的 options.length 降到 6 位,冲突概率 1/16^6 ≈ 1/16M,可接受;同时用 grunt-concurrent 把任务拆到 4 进程,构建时间从 180s 降到 45s,满足“大促前紧急发包”的极限场景。