如何为每个主题变量文件生成独立 CSS 并压缩

解读

国内中大型企业(如电商、SaaS、B 端后台)普遍需要多主题换肤能力:同一套组件库,通过切换变量文件即可产出“默认主题”“深色主题”“品牌红”等多套样式。
面试官想验证两点:

  1. 你是否能用 Grunt 把“一个入口 SCSS + N 个变量文件”映射成“N 份独立 CSS”;
  2. 你是否能在一次 Grunt 任务里完成编译→自动补前缀→压缩→重命名→指纹→通知全链路,且保证增量构建速度团队协作一致性
    如果仅回答“用 grunt-contrib-sass 循环”只能拿 60 分;必须体现动态多目标配置、并发性能、缓存、错误熔断、国内镜像源等工程化思维才能拿到 90+。

知识点

  1. 动态任务注册:grunt.config.merge + filesArray 映射,避免手写 20 个重复任务。
  2. 并发与缓存:grunt-concurrent + grunt-newer,把 node-sass 的 5s 编译降到 1s 内。
  3. 国内网络优化:sass 二进制镜像、postcss 插件走 cnpm 镜像源,防止 CI 卡死。
  4. 主题变量注入:利用 sass 的 !defaultdata 选项,把变量文件以 @import "variables/{{theme}}" 方式注入,保证一份入口文件即可。
  5. 压缩与指纹:grunt-postcss 内置 cssnano,再接入 grunt-filerev 解决 CDN 缓存。
  6. 错误熔断:grunt-contrib-watch 配合 grunt-eventemitter,sass 编译一旦报错立即弹系统通知并阻断后续压缩,防止脏文件上线。
  7. 团队规范:通过 grunt-eslint 校验 Gruntfile 本身,防止新人把任务写死;配合 husky 在 pre-commit 阶段强制跑 grunt themes,保证合并前主题包已生成

答案

  1. 目录约定
src/
  scss/
    index.scss          // 唯一入口,顶部留空行供 grunt 注入 @import "variables/{{theme}}"
    variables/
      default.scss
      dark.scss
      brand-red.scss
  1. 安装核心依赖(国内镜像
SASS_BINARY_SITE=https://npmmirror.com/mirrors/node-sass npm i -D grunt-sass@^3.1.0 sass postcss postcss-cli cssnano grunt-postcss grunt-concurrent grunt-newer grunt-contrib-clean grunt-contrib-rename grunt-filerev
  1. Gruntfile.js 关键片段(动态多目标
module.exports = function(grunt) {
  const themeList = grunt.file.expand('src/scss/variables/*.scss')
                       .map(f => f.replace(/.*\/(.*)\.scss/, '$1'));

  // 1. 动态生成 sass 子任务
  const sassTasks = {};
  themeList.forEach(t => {
    sassTasks[t] = {
      options: {
        implementation: require('sass'),
        sourceMap: true,
        // 关键:把变量文件注入到入口顶部
        data: `@import "variables/${t}";\n${grunt.file.read('src/scss/index.scss')}`
      },
      files: [{
        expand: true,
        cwd: 'src/scss',
        src: 'index.scss',
        dest: '.tmp/css',
        ext: `-${t}.css`
      }]
    };
  });
  grunt.config.merge({ sass: sassTasks });

  // 2. 压缩 + 自动前缀
  grunt.config.merge({
    postcss: {
      options: {
        processors: [
          require('autoprefixer')({ overrideBrowserslist: ['> 1%', 'last 2 versions'] }),
          require('cssnano')({ preset: 'default' })
        ]
      },
      dist: {
        expand: true,
        cwd: '.tmp/css',
        src: '*.css',
        dest: 'dist/themes/',
        ext: '.min.css'
      }
    }
  });

  // 3. 并发加速
  grunt.config.merge({
    concurrent: {
      themes: themeList.map(t => `sass:${t}`)
    }
  });

  // 4. 指纹
  grunt.config.merge({
    filerev: {
      themes: { src: 'dist/themes/*.min.css', dest: 'dist/themes/' }
    }
  });

  // 5. 任务流
  grunt.registerTask('themes', [
    'clean:.tmp',
    'newer:concurrent:themes',
    'postcss',
    'filerev',
    'clean:.tmp'
  ]);

  // 6. watch 开发模式
  grunt.config.merge({
    watch: {
      themes: {
        files: ['src/scss/**/*.scss'],
        tasks: ['themes'],
        options: { spawn: false, atBegin: true }
      }
    }
  });

  grunt.loadNpmTasks('grunt-sass');
  grunt.loadNpmTasks('grunt-postcss');
  grunt.loadNpmTasks('grunt-concurrent');
  grunt.loadNpmTasks('grunt-newer');
  grunt.loadNpmTasks('grunt-contrib-clean');
  grunt.loadNpmTasks('grunt-filerev');
  grunt.loadNpmTasks('grunt-contrib-watch');
};
  1. 运行
npx grunt themes        // CI 构建
npx grunt watch:themes  // 本地开发

产出

dist/themes/
  index-default-8e9a2e.min.css
  index-dark-3c4d5e.min.css
  index-brand-red-f7a1bd.min.css

拓展思考

  1. 组件库按需引入:若项目使用 babel-plugin-import 按需加载组件,需把主题 CSS 也拆成组件级粒度,可再封装 grunt-substrate 插件,按组件维度并行编译,避免全量主题包过大。
  2. CSS 变量降级:国内仍需兼容 IE11,可用 postcss-custom-properties 把 CSS 变量静态化,同时保留变量文件供现代浏览器动态切换,实现渐进式换肤
  3. 与 Vite/Webpack 共存:老项目用 Grunt,新项目用 Vite,可通过统一 design-token 仓库(仅存放 variables 文件)+ npm workspaces,保证两套构建工具共用同一变量源,避免设计师改色值后两边不同步。