配置 grunt-stylelint 检查 SCSS 嵌套深度并自动修复

解读

面试官抛出这道题,并不是单纯让你装个插件,而是想验证三件事:

  1. 你是否真正理解 Grunt 的“配置即代码”哲学,能把 stylelint 的 CLI 能力无缝嫁接到 Grunt 任务流;
  2. 面对国内常见的深度嵌套 SCSS 遗产代码,你能否给出可落地、可渐进、可灰度的自动化修复方案,而不是一句“跑 --fix 就行”;
  3. 你是否知道在多人协作、CI 门禁、husky 预提交等中国厂内场景下,如何让 grunt-stylelint 既当“守门员”又当“清道夫”,不阻塞流水线,也能把嵌套深度逐步降到团队规约(≤3 层)以内

知识点

  1. grunt-stylelint 与原生 stylelint 的“任务化”差异

    • grunt-stylelint 把 stylelint 的 CLI 参数映射成 Grunt 的 options 字段,必须显式开启 fix: true 才会回写磁盘
    • formatter 只能取 json/string/verbose国内很多团队误配成 compact 会直接抛 UNKNOWN_OPTION,导致 CI 异常退出。
  2. 嵌套深度规则的唯一 ID

    • stylelint 社区插件 stylelint-max-nesting-depth 已被官方吸收,规则名称为 max-nesting-depth阈值推荐 3忽略 @media/@supports 需配 { ignoreAtRules: ['media', 'supports'] },否则移动端响应式代码会大面积报错。
  3. 自动修复的“安全域”

    • stylelint 的 --fix 只能处理可自动修正的规则(如 declaration-colon-space-after),max-nesting-depth 属于“不可自动修复”规则
    • 因此**“自动修复”在嵌套深度场景下本质是“先报错,再人工重构”**,Grunt 任务链里必须再接一个 grunt-shellgrunt-exec 调用自定义脚本,把超深嵌套自动拆成 @mixin&- 形式,才能真正落地“无人值守”
  4. 国内 CI 门禁最佳实践

    • .stylelintignore先兜底 src/legacy/**/*.scss分阶段把嵌套深度>3 的文件列入 nesting-depth-fixme 清单
    • 通过 grunt-contrib-watch 监听 src/**/*.scss只对本次 MR 的 diff 文件执行 grunt-stylelint增量拦截新增嵌套债务
    • huskypre-commit 钩子中grunt stylelint:diff --filter=git 做最后守门保证本地修复与远端流水线结果零差异

答案

  1. 安装依赖(国内源加速
npm i -D grunt-stylelint stylelint-config-standard-scss stylelint-config-rational-order stylelint-max-nesting-depth
# 如果淘宝源 502,切到华为云或自建 Nexus
  1. 在项目根新建 .stylelintrc.jsCommonJS 写法,兼容老版本 Node12
module.exports = {
  extends: [
    'stylelint-config-standard-scss',
    'stylelint-config-rational-order'
  ],
  plugins: ['stylelint-max-nesting-depth'],
  rules: {
    // **嵌套深度 ≤3,忽略媒体查询**
    'max-nesting-depth': [3, { ignoreAtRules: ['media', 'supports'] }],
    // 其余规则按团队规约追加
  }
};
  1. Gruntfile.js(只保留核心任务,避免过度抽象
module.exports = function(grunt) {
  grunt.initConfig({
    stylelint: {
      options: {
        configFile: '.stylelintrc.js',
        formatter: 'string',
        ignoreDisables: false,
        failOnError: true,
        // **fix 开关:对可修复规则生效,对嵌套深度仅做检查**
        fix: true,
        // **国内 CI 常见坑:报告文件必须 UTF-8,否则 GitLab 乱码**
        reportOutput: './reports/stylelint.json'
      },
      src: ['src/**/*.scss', '!src/legacy/**/*.scss']
    },

    // **二次加工:把超深嵌套自动拆 mixin**
    shell: {
      nestingRefactor: {
        command: 'node scripts/auto-mixin-nesting.js --max=3 --src=src'
      }
    },

    watch: {
      scss: {
        files: ['src/**/*.scss'],
        tasks: ['stylelint', 'shell:nestingRefactor']
      }
    }
  });

  grunt.loadNpmTasks('grunt-stylelint');
  grunt.loadNpmTasks('grunt-shell');
  grunt.loadNpmTasks('grunt-contrib-watch');

  // **默认任务:先检查,再人工触发重构**
  grunt.registerTask('default', ['stylelint']);
  // **上线前强制清零嵌套债务**
  grunt.registerTask('lint:fix', ['stylelint', 'shell:nestingRefactor', 'stylelint']);
};
  1. 灰度脚本 scripts/auto-mixin-nesting.js核心思路:AST 遍历,>3 层抽成 @mixin
const postcss = require('postcss');
const scssParser = require('postcss-scss');
const fs = require('fs');
const path = require('path');

function refactor(file, maxDepth = 3) {
  const css = fs.readFileSync(file, 'utf8');
  const root = postcss.parse(css, { syntax: scssParser });
  root.walkRules(rule => {
    const depth = rule.selector.split('&').length - 1;
    if (depth > maxDepth) {
      // **生成 mixin 名:基于文件路径+行号,避免冲突**
      const mixinName = `mixin-${path.basename(file, '.scss')}-${rule.source.start.line}`;
      const mixin = postcss.atRule({ name: 'mixin', params: mixinName });
      rule.each(node => mixin.append(node.clone()));
      rule.removeAll().append(postcss.decl({ prop: '@include', value: mixinName + ';' }));
      root.prepend(mixin);
    }
  });
  fs.writeFileSync(file, root.toString());
}

// CLI 入口
process.argv.slice(2).forEach(file => refactor(file, 3));
  1. 本地验证 & 提交
grunt default                 # 先过检查
grunt lint:fix                # 自动拆 mixin 再二次检查
git add . && git cz           # 用 commitizen 生成符合 Angular 规范的提交

拓展思考

  1. 当团队切到 Vite/Rollup 后,Grunt 任务是否还有价值?
    国内很多存量项目仍跑在 Jenkins+Grunt 的古典流水线,直接迁移成本高于渐进融合。可以把 grunt-stylelint 包装成 “质量门禁微服务”通过 HTTP 暴露 /lint-diff 接口让 Vite 的 buildStart 钩子远程调用实现新框架老任务共存保护企业原有 CI 资产

  2. 如何量化“嵌套深度”带来的技术债务?
    .stylelint.json 报告基础上,写一条 grunt-custom-task 把每条 max-nesting-depth 告警转成 SonarQube 的 issue 格式通过 sonar-webhook 推送到内部 Tech Radar 看板每周例会用数据驱动重构排期让产品经理也看得懂“嵌套债”对交付效率的负面 ROI

  3. 如果未来 stylelint 官方把 max-nesting-depth 做成可自动修复,Grunt 侧需要改什么?
    只需fix: true 保留移除自定义 shell 任务并在 Gruntfile 里加一段 if (grunt.option('refactor')) 开关即可在官方方案发布后秒级切换保证团队规范与社区演进同步

记住:“能跑”不等于“能落地”,能灰度、可度量、可回滚,才是国内面试官想听的 Grunt 深度答案。