使用 grunt-filerev 为静态资源添加 MD5 哈希并生成映射表
解读
在国内前端工程化面试中,资源缓存与版本管理是高频考点。面试官抛出“用 grunt-filerev 加 MD5 哈希”这一题,表面看是插件使用,实则想验证候选人是否理解:
- 缓存击穿与版本号策略(为什么用哈希而非时间戳)
- Grunt 插件链式协作(filerev 与 usemin、cdnify、uglify 如何配合)
- 映射表落地方式(如何被后端模板或 Node 中间件消费)
若只答“装插件、配任务”,会被追问“映射表怎么用”“CSS 中 url() 如何自动替换”,因此答案必须覆盖配置、替换、消费三环节,并给出国内主流场景的落地细节。
知识点
-
grunt-filerev 核心机制
对指定文件计算 MD5 并改写文件名,生成dest/filerev.json映射表,格式为{ "原始路径": "带哈希路径" }。 -
Grunt 任务阶段与文件对象流
filerev 属于 “编译后处理” 阶段,需保证在 uglify、imagemin 之后,useminPrepare/usemin 之前,否则哈希会重复或路径不一致。 -
映射表消费方案
国内常见三种:- Node 中间件:Egg、Koa 在静态资源服务中间件里读映射表,模板引擎直接用
helper.asset('a.js')输出带哈希路径。 - JSP/Velocity 方案:Gradle 或 Jenkins 构建时把映射表注入到
global.jsMap,前端用${jsMap.get("a.js")}拼链接。 - 前端自动替换:grunt-usemin 读取映射表,一次性把 HTML/CSS/JS 中的引用全部改写,适合纯静态站点托管在阿里云 OSS+CDN 场景。
- Node 中间件:Egg、Koa 在静态资源服务中间件里读映射表,模板引擎直接用
-
路径对齐与 publicPath
若项目采用“源码—中间产物—发布目录”三级结构(src/.tmp/dist),必须在 filerev 中设置options: { basePath: 'dist/', algorithm: 'md5', length: 8 },保证映射表 key 不带多余前缀,否则后端拼接域名会 404。 -
并发与增量
大型项目(如电商活动页 2000+ 图片)需开启grunt-newer与filerev的options: { algorithm: 'md5', length: 6 }降低 CPU 占用,同时把映射表缓存到.grunt/grunt-filerev-cache.json,实现增量构建,符合国内“下班前必须出包”的强节奏。
答案
-
安装与版本锁定
npm i -D grunt-filerev@2.3.1 # 锁定版本,避免 3.x 的 API 差异 -
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' ]); }; -
后端消费示例(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> -
上线验证
在 Jenkins 流水线中加一步grep -q 'app.[a-f0-9]{8}.js' dist/index.html || exit 1,确保哈希真正被注入,避免“构建成功但缓存未击穿”的线上事故。
拓展思考
-
若团队已迁移到 webpack,是否还需要 grunt-filerev?
国内很多存量项目(如银行、运营商后台)仍用 Grunt 流水线,直接重写风险高。可采用**“双轨构建”:新业务用 webpack 5 模块联邦,老业务继续 grunt-filerev,通过共享映射表**让同一页面混合引用两种资源,实现渐进式迁移。 -
如何支持“雪碧图”或“字体图标”自动哈希?
grunt-filerev 默认不处理?v=hash查询串,需手写 grunt-string-replace 任务,把url(icon.woff?v=#rev#)替换成url(icon.3f4e5a6b.woff),否则 HTTP2 环境下字体缓存仍按旧名失效。 -
灰度发布与回滚
国内大厂常把映射表上传到 Redis,灰度阶段只让 10% 机器读新映射表。回滚时只需把 Redis key 指回旧表,无需重新打包,保证“秒级回滚”能力,这也是面试官想听到的工程化深度。 -
性能极限优化
对于 10W+ 小图标的活动会场,可把 filerev 的options.length降到 6 位,冲突概率 1/16^6 ≈ 1/16M,可接受;同时用grunt-concurrent把任务拆到 4 进程,构建时间从 180s 降到 45s,满足“大促前紧急发包”的极限场景。