解释在 grunt 中实现静态资源永久缓存

解读

面试官抛出“永久缓存”话题,本质想验证两点:

  1. 你是否理解浏览器缓存策略(Expires/Cache-Control/ETag/Last-Modified)以及“指纹文件名”才是彻底避免 304 请求、实现“永久”缓存的核心;
  2. 你是否能在 Grunt 体系里把“文件名加哈希替换引用路径配合长缓存头”三步跑通,并兼顾国内常见 CDN、Nginx、OSS 等部署场景。
    回答时务必先点破“指纹 + 长缓存头”缺一不可,再给出 Grunt 插件组合与配置细节,最后补一句“上线回滚策略”,即可体现工程化深度。

知识点

  1. 浏览器缓存:Cache-Control: max-age=31536000, immutable 与 304 对比
  2. 指纹策略:文件内容 → MD5 → 8~10 位哈希 → 嵌入文件名(app.3ab4c6.js)
  3. Grunt 插件:
    • grunt-rev / grunt-filerev:生成带哈希的新文件
    • grunt-usemin:扫描 html/css/js 中的 build:js / build:css 注释块,自动替换引用
    • grunt-contrib-copy / clean:保证 dist 干净
    • grunt-asset-manifest(可选):输出 json 映射表,供后端模板引擎动态读取
  4. 国内 CDN:阿里云 OSS、腾讯云 COS、七牛均支持“忽略参数缓存”,必须关闭“缓存参数过滤”,否则 app.js?v=3ab4c6 仍会被当作不同文件
  5. 回滚:保留最近两版带哈希文件,Nginx 做 try_files $uri $uri/ /index.html,避免 404

答案

步骤一:安装核心插件
npm i -D grunt-filerev grunt-usemin grunt-contrib-concat grunt-contrib-uglify grunt-contrib-cssmin grunt-contrib-clean

步骤二:Gruntfile.js 关键片段

module.exports = function(grunt) {
  grunt.initConfig({
    clean: { dist: 'dist' },

    // 1. 先合并、压缩
    concat: {
      js: { src: ['src/js/*.js'], dest: 'dist/tmp/app.js' },
      css: { src: ['src/css/*.css'], dest: 'dist/tmp/app.css' }
    },
    uglify: { js: { files: { 'dist/tmp/app.min.js': 'dist/tmp/app.js' } } },
    cssmin: { css: { files: { 'dist/tmp/app.min.css': 'dist/tmp/app.css' } } },

    // 2. 加哈希 → 永久缓存文件名
    filerev: {
      options: { encoding: 'utf8', algorithm: 'md5', length: 8 },
      assets: {
        src: ['dist/tmp/app.min.*'],
        dest: 'dist/static/'
      }
    },

    // 3. 替换 html 里的引用
    usemin: {
      html: 'dist/*.html',
      options: {
        assetsDirs: ['dist/static'],
        patterns: {
          js: [[/app\.min\.js/, 'Replacing app.min.js']]
        }
      }
    }
  });

  grunt.registerTask('cache', [
    'clean', 'concat', 'uglify', 'cssmin',
    'filerev', 'usemin'
  ]);
};

步骤三:Nginx 配置(国内云主机最常见)

location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2)$ {
    root /var/www/dist;
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    gzip_static on;
}

步骤四:上线验证

  1. 运行 grunt cache 后,dist 目录出现 app.3ab4c6.min.js
  2. 打开 Chrome Network,Status=200(from disk cache),无 304
  3. 修改业务代码再次构建,哈希变为 app.9e5d2a.min.js,旧文件保留,刷新页面加载新文件,实现平滑版本切换永久缓存并存

拓展思考

  1. 若团队使用服务端模板(如 JSP、Thymeleaf),可让 grunt-asset-manifest 生成 manifest.json,Java 启动时读入内存,模板里动态输出 <script src="${assets.appJs}">,避免全量替换 html
  2. 对于微前端多页面应用,可将 filerev 的 length 调到 6 位减少 url 长度,同时开启 grunt-filerev-summary 输出映射表,供子应用运行时异步加载
  3. 国内灰度发布场景,可结合 Node 中间层:把 manifest.json 写入 Redis,按用户维度返回不同哈希,实现“用户级缓存版本”,回滚只需改 Redis 键值,无需重新构建