使用 grunt-newer 仅编译被修改的 SASS 文件

解读

在国内一线/二线互联网公司的前端面试中,“只编译被修改的文件” 是考察候选人是否具备工程化性能优化意识的高频题。
面试官真正想听的是:

  1. 你能否准确描述 grunt-newer 的缓存机制(mtime 对比 + .cache 文件)。
  2. 你能否在 Gruntfile 中正确配置任务链,让 sass → newer:sass 形成增量编译闭环
  3. 你能否规避常见的“缓存穿透”陷阱(如依赖的 partial 被修改却未触发主文件重编)。
  4. 你能否给出可落地的 CI/CD 策略(如何在 Jenkins/GitLab Runner 中利用缓存加速)。
    回答时务必先讲原理,再给代码,最后补场景,体现“性能-可维护-可扩展”三角平衡。

知识点

  • grunt-newer 的缓存键:以 src 文件的绝对路径 + mtime 做 MD5,缓存在 node_modules/grunt-newer/.cache 目录。
  • 任务别名链grunt.registerTask('dev', ['newer:sass', 'newer:postcss', 'browserSync']) 保证多任务级联增量
  • partial 依赖问题:SASS 的 @import '_partial' 被改动时,仅 newer 无法感知上层文件,需配合 grunt-sass-globbingimport-once 做依赖图分析。
  • CI 缓存一致性:在 Docker 构建镜像中,必须把 .cache 目录挂载到宿主机 volume,否则每次构建都全量。
  • 时间戳漂移:国内 Windows/WSL 混合开发时,mtime 精度差异会导致 newer 误判,需 grunt-newer-options: override 强制时间窗口容忍 2000 ms。

答案

  1. 安装依赖
npm i -D grunt-sass grunt-newer sass
  1. Gruntfile.js 关键配置
module.exports = function(grunt) {
  grunt.initConfig({
    sass: {
      dev: {
        options: { implementation: require('sass'), sourceMap: true },
        files: [{
          expand: true,
          cwd: 'src/scss',
          src: ['**/*.scss', '!**/_*.scss'],   // **排除 partial,只编译入口文件**
          dest: 'dist/css',
          ext: '.css'
        }]
      }
    },

    newer: {
      options: {
        cache: '.grunt-newer-cache',          // **显式指定缓存目录,方便 CI 持久化**
        tolerance: 2000                       // **国内常见 2 秒 mtime 误差容忍**
      }
    }
  });

  grunt.loadNpmTasks('grunt-newer');
  grunt.loadNpmTasks('grunt-sass');

  // **默认任务:增量编译 + 监听**
  grunt.registerTask('default', ['newer:sass', 'watch']);
  grunt.registerTask('watch', function() {
    grunt.config('watch', {
      scss: {
        files: ['src/scss/**/*.scss'],
        tasks: ['newer:sass']
      }
    });
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.task.run('watch');
  });
};
  1. 解决 partial 修改不触发问题
    src/scss/main.scss 使用 @import 'components/button' 时,新增 grunt-contrib-watch 的 event: 'changed' 钩子,当 _button.scss 改动,强制 touch 主文件以更新其 mtime,确保 newer 重新编译。
grunt.event.on('watch', function(action, filepath) {
  if (filepath.includes('_') && filepath.endsWith('.scss')) {
    const mainFile = 'src/scss/main.scss';
    require('fs').utimesSync(mainFile, new Date(), new Date());
  }
});
  1. CI 加速示例(GitLab Runner)
cache:
  paths:
    - .grunt-newer-cache/
script:
  - npm ci
  - npx grunt newer:sass --cache-dir=.grunt-newer-cache

拓展思考

  • 与 Vite/Webpack 的 HMR 对比:grunt-newer 基于文件系统,无内存级缓存,在 500+ 模块以上项目性能劣于 esbuild 的 O(1) 差分编译;但在存量 jQuery/多页应用中,零改造成本是其最大优势。
  • 混合语言场景:若项目同时存在 SASS、Less、Stylus,统一用 grunt-newer 做顶层调度,底层分别调用 grunt-sass、grunt-contrib-less、grunt-contrib-stylus,保证缓存目录隔离,避免 hash 冲突。
  • 云端协同:在阿里云效 Flow 中,将 .grunt-newer-cache 存入 OSS 缓存桶,利用 cache-key: ${CI_COMMIT_REF_SLUG} 实现分支级增量,可将 3 分钟的全量构建降至 30 秒,节省 80% 构建时长