配置 grunt-stylelint 检查 SCSS 嵌套深度并自动修复
解读
面试官抛出这道题,并不是单纯让你装个插件,而是想验证三件事:
- 你是否真正理解 Grunt 的“配置即代码”哲学,能把 stylelint 的 CLI 能力无缝嫁接到 Grunt 任务流;
- 面对国内常见的深度嵌套 SCSS 遗产代码,你能否给出可落地、可渐进、可灰度的自动化修复方案,而不是一句“跑 --fix 就行”;
- 你是否知道在多人协作、CI 门禁、husky 预提交等中国厂内场景下,如何让 grunt-stylelint 既当“守门员”又当“清道夫”,不阻塞流水线,也能把嵌套深度逐步降到团队规约(≤3 层)以内。
知识点
-
grunt-stylelint 与原生 stylelint 的“任务化”差异:
- grunt-stylelint 把 stylelint 的 CLI 参数映射成 Grunt 的
options字段,必须显式开启fix: true才会回写磁盘; - 其
formatter只能取json/string/verbose,国内很多团队误配成compact会直接抛 UNKNOWN_OPTION,导致 CI 异常退出。
- grunt-stylelint 把 stylelint 的 CLI 参数映射成 Grunt 的
-
嵌套深度规则的唯一 ID:
- stylelint 社区插件
stylelint-max-nesting-depth已被官方吸收,规则名称为max-nesting-depth,阈值推荐 3,忽略@media/@supports需配{ ignoreAtRules: ['media', 'supports'] },否则移动端响应式代码会大面积报错。
- stylelint 社区插件
-
自动修复的“安全域”:
- stylelint 的
--fix只能处理可自动修正的规则(如declaration-colon-space-after),max-nesting-depth属于“不可自动修复”规则; - 因此**“自动修复”在嵌套深度场景下本质是“先报错,再人工重构”**,Grunt 任务链里必须再接一个
grunt-shell或grunt-exec调用自定义脚本,把超深嵌套自动拆成@mixin或&-形式,才能真正落地“无人值守”。
- stylelint 的
-
国内 CI 门禁最佳实践:
- 在
.stylelintignore里先兜底src/legacy/**/*.scss,分阶段把嵌套深度>3 的文件列入nesting-depth-fixme清单; - 通过
grunt-contrib-watch监听src/**/*.scss,只对本次 MR 的 diff 文件执行 grunt-stylelint,增量拦截新增嵌套债务; - 在
husky的pre-commit钩子中用grunt stylelint:diff --filter=git做最后守门,保证本地修复与远端流水线结果零差异。
- 在
答案
- 安装依赖(国内源加速)
npm i -D grunt-stylelint stylelint-config-standard-scss stylelint-config-rational-order stylelint-max-nesting-depth
# 如果淘宝源 502,切到华为云或自建 Nexus
- 在项目根新建
.stylelintrc.js(CommonJS 写法,兼容老版本 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'] }],
// 其余规则按团队规约追加
}
};
- 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']);
};
- 灰度脚本
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));
- 本地验证 & 提交
grunt default # 先过检查
grunt lint:fix # 自动拆 mixin 再二次检查
git add . && git cz # 用 commitizen 生成符合 Angular 规范的提交
拓展思考
-
当团队切到 Vite/Rollup 后,Grunt 任务是否还有价值?
国内很多存量项目仍跑在 Jenkins+Grunt 的古典流水线,直接迁移成本高于渐进融合。可以把 grunt-stylelint 包装成 “质量门禁微服务”,通过 HTTP 暴露/lint-diff接口,让 Vite 的buildStart钩子远程调用,实现新框架老任务共存,保护企业原有 CI 资产。 -
如何量化“嵌套深度”带来的技术债务?
在.stylelint.json报告基础上,写一条grunt-custom-task把每条max-nesting-depth告警转成 SonarQube 的issue格式,通过sonar-webhook推送到内部 Tech Radar 看板,每周例会用数据驱动重构排期,让产品经理也看得懂“嵌套债”对交付效率的负面 ROI。 -
如果未来 stylelint 官方把
max-nesting-depth做成可自动修复,Grunt 侧需要改什么?
只需把fix: true保留,移除自定义shell任务,并在Gruntfile里加一段if (grunt.option('refactor'))开关,即可在官方方案发布后秒级切换,保证团队规范与社区演进同步。
记住:“能跑”不等于“能落地”,能灰度、可度量、可回滚,才是国内面试官想听的 Grunt 深度答案。