Monorepo下Stylelint样式检查配置实战与避坑指南
发布时间:2026/9/20 15:56:28 作者:尧图编辑部 阅读量:1,286

最近给团队搭一套多包前端仓库的工程化模板整体框架和构建链路敲定得很快反倒是在 CSS 这一类静态检查上磨了最久。原因不是规则配不出来而是 Monorepo 环境里样式检查的边界比单仓复杂得多——哪些包要查、SCSS 怎么解析、要不要在提交前自动修复、怎么跟已有的 Prettier 共存每一条都能踩出完全不同的坑。这篇文章就以 Stylelint 这条线为主完整记录我从零搭建这套模板时的设计思路、实际配置和复盘出来的避坑点给同样在做企业级前端基建的同学一份可以直接“抄作业”的清单。开始之前先给本文定个位。阅读对象主要是这三类人一是准备把仓库拆成多包结构、需要统一工程化出口的技术负责人二是已经在用 Monorepo、但发现样式检查经常漏检或误报、想补齐规范体系的同学三是纯新手想搞清楚 Stylelint 在现在这个版本到底该怎么配、哪些网上教程已经过期了。文章里会用实际文件、命令和报错来展开不是我拍脑袋想出来的理论都是这套模板从“能跑”到“跑得稳”过程中的真实记录。1. Monorepo 工程化模板的整体设计先想清楚 Stylelint 的边界1.1 模板要解决的三个核心问题搭建 Monorepo 模板有一说一最大的挑战不是选型而是把“约定大于配置”这几个字真正落地。单仓时代一个项目一个配置改坏了最多影响自己到了 Monorepo根目录一份配置会被十几个子包继承任何一处埋雷都会放大。我当时对这套模板定的核心目标是三个。第一保证所有子包的 CSS/SCSS 代码长一个样。多个业务包分别由不同小组维护如果没有统一规范就会出现 A 包属性排序按字母、B 包按盒子模型、C 包干脆不排的情况。Stylelint 在这里承担的是“同一把尺子”的角色规则写得再细也得让每个包执行起来完全一致。第二把检查成本压到最低。Monorepo 的 lint 通常会在 CI 里全量跑一遍样式文件多了以后纯靠 Stylelint 全量扫描会拖慢流水线。所以我比较在意 cache 能力、增量检查能力和针对单个包做定向 lint 的能力这些在模板阶段就要设计好而不是等项目膨胀后再补。第三尽量少出现“在 A 包跑得好好的到 B 包就报错”的情况。这个问题通常不是规则本身的问题而是依赖解析和配置继承的边界没划清楚。Styelint 在 Monorepo 下的表现极其依赖 Node 模块解析路径如果某个插件只在子包里装了、根配置又引用了它那这个子包能跑、其他子包就跑不了排查起来非常痛苦。为了把这三个问题说清楚我用一套轻量的 pnpm workspace 结构来举例。仓库大致长这样my-monorepo/ ├── package.json ├── pnpm-workspace.yaml ├── turbo.json ├── stylelint.config.mjs ├── .stylelintignore ├── .lintstagedrc.mjs └── packages/ ├── ui/ │ ├── src/ │ │ ├── Button/ │ │ │ ├── index.tsx │ │ │ └── index.scss │ │ └── theme/ │ │ └── variables.scss │ └── package.json ├── utils/ │ └── package.json └── web/ ├── src/ └── package.json根目录负责放所有跟脚手架、规范、流水线有关的配置子包只保留业务开发内容。Stylelint 的配置文件放在根目录通过 glob 作用到所有 packages 下的样式文件。这样做的优势很直观新增子包时不需要重新发明一套 lint 配置天然享受根配置的“全局默认”效果。1.2 Stylelint 在 Lint 流水线中的位置我见过很多团队把 Stylelint 当成“周末有空才跑一下”的工具这是非常危险的。样式代码的劣化是渐进的今天少写一个分号、明天属性顺序乱一点后天就有人开始用嵌套强行铺样式最终整个样式系统会变得不可维护。在这套模板里样式检查被硬性嵌进了三层流水线。本地开发阶段编辑保存时通过 VSCode 插件自动执行 Stylelint配合--fix参数在保存瞬间把能修的问题修掉。提交阶段通过 husky lint-staged只对 staged 的样式文件做增量检查保证进入仓库的每一行代码都已经过过滤。CI 阶段通过 Turbo 的任务编排对所有包统一执行lint任务并把警告数量上限设成 0有警告就算不通过。这样安排的逻辑很清楚能自动修的绝不让开发者手动改能在本地拦截的绝不留到 Code Review 里靠人眼去挑能在小范围内快速查的绝不全仓库拖网。Stylelint 是这一整套流程里唯一一个专职盯样式文件的角色它的配置质量直接决定这套流水线是“有用”还是“添乱”。2. 依赖选型与版本适配Stylelint v16 生态怎么搭配2.1 Stylelint v16 到底变了什么这一节必须单独拿出来说因为网上大量教程还停留在 v14、v15 时期如果你照着他们写的配置去用最新版轻则一堆报错重则整个 lint 根本跑不起来。Stylelint v16 有几个和 Monorepo 模板强相关的破坏性变更这里先说最关键的三个。第一个默认启用了 flat config老式的.stylelintrc写法虽然还能用但已经不是推荐路径。新写法是stylelint.config.mjs文件导出配置对象规则、插件、自定义语法全部平铺在对象里。模板如果用 ESM默认就应当走 flat config 这套。第二个移除了所有 stylistic纯风格类内置规则比如缩进用几个空格、引号用单还是双、冒号后面要不要空格这些全都不再管了。官方给出的信号很明确纯格式问题交给 PrettierStylelint 只负责那些 Prettier 做不了的“代码质量类”检查比如属性顺序、单位合法性、颜色表示法、重复选择器这类规则。第三个默认忽略node_modules以及版本管理工具忽略的文件。听着是改善实际上埋了个坑如果你在 glob 里写匹配范围时不够精确或者想要检查某些被.gitignore排除的生成文件会发现 Stylelint 完全不理会排查半天才意识到是 v16 的默认行为。另外还有一个温和但很重要的变化stylelint-config-prettier这个配置包的定位也变了。因为 v16 已经移除了风格类规则它需要关掉的冲突规则变少了但对于从老版本升级上来的团队它仍然建议保留避免历史规则残留造成冲突。2.2 我的依赖清单与选型理由根据上面这些变化我在模板里用的依赖组合是这样一套全部放在根目录的devDependencies里{ devDependencies: { stylelint: ^16.12.0, stylelint-config-standard-scss: ^14.0.0, stylelint-config-clean-order: ^6.0.0, postcss-scss: ^4.0.9 } }逐个说一下为什么是这四个以及为什么不需要更多。stylelint本身是核心引擎唯一指定直接上最新版。postcss-scss是 SCSS 语法解析器因为 Stylelint 默认只能理解标准 CSS 语法如果不装它遇到 SCSS 里的变量、嵌套、mixin 这些特性会直接解析失败。stylelint-config-standard-scss是官方推荐配置的 SCSS 扩展包它自带了一组覆盖面很广的规则等于帮你把“哪些写法要禁止”这个最耗精力的决策做完了。stylelint-config-clean-order是专门用来做属性排序的预设方案底层依赖stylelint-order插件但比你自己手写一套属性顺序要省事得多它里面已经排好了 box model、布局、排版等不同区块的顺序。有人可能会问为什么不用stylelint-order直接配因为企业级模板要的不是“能用”而是“别人接手也能维护”。手写属性顺序看起来灵活实际维护成本极高团队里每个人都可能按自己的习惯往里加属性最后顺序表会变得越来越不可解释。用预设方案虽然牺牲了一点定制空间但换来的是可预期性和低维护成本我觉得这笔账非常划算。还有一个经常被塞进来的包是stylelint-prettier但我最终没有选它。原因很简单它会让你在跑 Stylelint 时把 Prettier 的格式检查也跑一遍两个工具的责任边界会立刻模糊掉。我的原则是Prettier 管格式、Stylelint 管代码质量两边各干各的只在提交前的管线里串行执行不让它们在同一个进程里互相干扰。3. 实操落盘根配置、子包配置与编辑器集成3.1 根目录 stylelint.config.mjs 的写法先给出一份我在模板里实际使用的完整配置然后逐段拆解。// stylelint.config.mjs export default { extends: [ stylelint-config-standard-scss, stylelint-config-clean-order, ], customSyntax: postcss-scss, rules: { number-leading-zero: always, unit-allowed-list: [px, rem, em, %, vh, vw, s, ms], at-rule-no-unknown: [ true, { ignoreAtRules: [include, mixin, use, forward, tailwind, apply, screen, layer, variants, responsive], }, ], scss/at-rule-no-unknown: [ true, { ignoreAtRules: [tailwind, apply, screen, layer, variants, responsive], }, ], }, ignoreFiles: [ **/node_modules/**, **/dist/**, **/coverage/**, **/*.min.css, **/*.map, ], };这份配置里有几个值得专门说清楚的细节。extends数组的赋值顺序是有讲究的。先放stylelint-config-standard-scss它提供基础规则再放stylelint-config-clean-order它负责定义属性排序。后加载的配置会覆盖前者的同名规则所以如果两个配置对某条规则有不同定义最终以靠后的为准。这也是我在这里保证“排序规则优先”的一个小技巧。customSyntax必须显式声明。很多教程会教你直接把postcss-scss当syntax配但 v16 的 flat config 里它已经变成了顶层属性customSyntax名字和位置都变了。如果漏配SCSS 文件会被当作纯 CSS 解析变量、嵌套、mixin 那类语法会直接变成一串看不懂的报错。这一点是新手最容易懵的地方。unit-allowed-list是我在公司项目里一定会加的一条规则。很多团队会不小心在代码里混入pt、pc、in这类不适合 Web 的物理单位加上这个白名单后这类用法在 lint 阶段就会被直接拦下。当然白名单具体放哪些单位要看项目实际情况如果用到vw、vh、min-width之类也记得补上。at-rule-no-unknown和scss/at-rule-no-unknown这条是我特意为 SCSS 以及 Tailwind 场景加的豁免。如果你在项目里用到 Tailwind那么tailwind、apply、screen这些 at-rule 默认会被判为非法必须在忽略列表里声明。如果不小心只配了stylelint-config-standard-scss还要注意它的 SCSS 场景下use、forward等新模块语法可能会出现误报所以我在列表里一并做了解放。3.2 在 package.json 与 Turbo 中串联 lint有了配置文件还不够关键是把它串进日常的命令体系。我在根目录的package.json里定义了三层脚本。{ scripts: { lint: pnpm -r lint, lint:style: stylelint \packages/**/*.{css,scss}\ --cache --cache-location node_modules/.cache/.stylelintcache, lint:style:fix: stylelint \packages/**/*.{css,scss}\ --fix --cache --cache-location node_modules/.cache/.stylelintcache } }单独拎出来说lint:style这条命令里的两个参数。--cache是 Stylelint 的增量缓存能力它会把检查结果缓存到指定文件里下次只对改动过的文件重新检查。对 Monorepo 这种动辄几千个样式文件的仓库来说这个参数能让本地命令从十几秒直接降到一两秒。--cache-location把缓存文件专门放到node_modules/.cache下这个目录通常已经进了.gitignore不会污染仓库也方便在 CI 里配置缓存。如果仓库使用了 Turbo我会在turbo.json里再定义一层任务编排让每个子包都能独立执行自己的 lint同时共享缓存结果。{ tasks: { lint: { dependsOn: [^lint] } } }这段配置的作用是当我要对某一个包执行pnpm lint时Turbo 会先检查它的依赖包里有没有 lint 任务有的话先跑依赖再跑当前包。这保证了一个子包的样式检查不会因为依赖包尚未就绪而出现虚假失败。子包侧不需要维护一套独立的 Stylelint 配置只需要在package.json里加一行脚本{ scripts: { lint: stylelint \src/**/*.{css,scss}\ } }因为 Stylelint 会从当前文件的目录向上逐层寻找配置文件最终找到根目录的stylelint.config.mjs所以子包天然继承根配置。这也意味着如果你在某个子包里放了一个同名配置文件它会覆盖根配置的逻辑这种“局部覆盖”能力我建议在团队规范里明确限制避免出现风格分裂。3.3 接入 lint-staged 与 VSCode工程的硬性约束最终要落到“提交前”这个关卡。我用 husky lint-staged 来实现配置文件如下。// .lintstagedrc.mjs export default { *.{css,scss}: [stylelint --fix --allow-empty-input], *.{js,jsx,ts,tsx,json,md,css,scss}: [prettier --write], };这里有一个非常实战的细节stylelint --fix和prettier --write是两条独立命令它们会串行执行。顺序上我故意让 Stylelint 先跑、Prettier 后跑原因很简单Stylelint 的--fix只修它自己规则里的问题不会管格式化Prettier 则会把整个文件重新排版。先做代码质量修复再做格式化两者目标不同、互不覆盖不会出现“改完又改回去”的拉锯。再配一下 VSCode 的自动保存修复这样开发者在本地就能提前消掉大部分问题。{ editor.formatOnSave: true, editor.codeActionsOnSave: { source.fixAll.stylelint: explicit }, stylelint.enable: true, stylelint.validate: [css, scss] }有几个坑可以提前说。第一个是stylelint.validate如果不写scssSCSS 文件在保存时不会触发修复这是最典型的漏配之一。第二个是如果你装了 VSCode 自带 CSS 格式化插件它可能会和 Stylelint 抢格式建议在项目级.vscode/settings.json里禁掉不必要的 CSS 扩展。第三个是最新版本 VSCode 里codeActionsOnSave的推荐写法是explicit如果你看的是老文章写true编辑器会提示并让你自己做一次迁移。4. 避坑指南Monorepo 环境下真正容易翻车的六个点4.1 坑一SCSS 语法解析始终不对报错全是乱码现象stylelint: unknown word或者提示Cannot parse selector但你打开 SCSS 文件怎么看都没问题。原因几乎都是customSyntax没配或者配了但没生效。Stylelint v16 的 flat config 里customSyntax必须直接放在配置对象的顶层。常见错误是写在rules里或者用了旧版的syntax字段这两种写法在新版本下都不生效。排查方法直接执行一条最小命令npx stylelint packages/ui/src/Button/index.scss如果报错信息里出现 “Failed to parse SCSS” 这类描述第一反应就去看customSyntax。确认配置无误后再检查postcss-scss是否真的被安装到了运行目录下Monorepo 里经常出现根目录能解析、子包目录解析不了的情况本质上是依赖提升带来的差异。4.2 坑二pnpm 严格依赖导致插件找不到现象在根目录执行 Stylelint 一切正常切换到某个子包目录执行同样的命令报错Cannot find module stylelint-scss或者Cannot find module postcss-scss。原因出在 pnpm 的依赖隔离策略。pnpm 不会像 npm 那样把所有依赖都平铺到根node_modules它默认只让项目直接声明的依赖可见。如果根配置里使用了某个插件但该插件没有在子包的命令解析路径上可见子包里的 Stylelint 就加载不到。解决方案有两个。一种是给模板制定一条铁律所有 lint 相关依赖全部放在根目录 devDependencies子包不单独安装。另一种是在子包内显式声明插件依赖比如子包自己的package.json里也加上postcss-scss。个人推荐前一种因为 lint 工具链本质上属于工程化能力统一由根目录管理最可控。4.3 坑三路径 glob 导致重复漏检现象新增子包后它的样式文件没有进入 lint 范围或者某个目录的样式文件被重复检查了两次。前者的原因通常是根脚本里的 glob 只匹配指定目录。比如一开始写packages/ui/src/**/*.scss后面加了packages/admin自然就漏报了。保险的做法是在根脚本里使用相对宽泛的匹配例如packages/*/src/**/*.{css,scss}同时用ignoreFiles在配置里排除哪些需要忽略的目录。后者的重复检查通常出现在既有通配符又有显式文件名的场景。比如同时传了packages/ui/src/**/*.scss和packages/ui/src/theme/variables.scss两个参数重复报错会干扰你的判断。建议统一用一份 glob 表达式描述范围不做文件级追加。4.4 坑四Prettier 和 Stylelint 同时开启后互相打架现象本地保存后代码被 Prettier 重排紧接着 Stylelint 又报了一堆格式错误或者反过来Stylelint fix 完的代码又被 Prettier 排回原样。根因是职责边界没划清楚。Stylelint v16 已经不处理缩进、引号这类纯格式问题了所以理论上规则冲突的规模和旧版本相比少了很多。但只要你的模板里还有老的规则残留比如declaration-block-trailing-semicolon、indentation这类已经不存在的旧规则情况就会很混乱。我建议在模板里做两件事。第一在stylelint.config.mjs中只保留代码质量类规则不写任何格式类规则。第二用stylelint-config-prettier做一次收尾它会把可能与 Prettier 冲突的规则全部关掉。你不需要手动去对照规则列表这个配置包就是帮你做“保险”的。4.5 坑五Tailwind 指令导致的 at-rule 误报现象项目引入 Tailwind 后tailwind base;和apply flex;被 Stylelint 判为错误甚至导致 pipeline 失败。原因很好理解Stylelint 默认情况下不识别 Tailwind 的自定义 at-rule。虽然stylelint-config-standard-scss已经对 SCSS 的use、mixin做了处理但 Tailwind 的tailwind、apply、screen不在它的考虑范围里。解决方式是在两份规则里同时放开即我在前面的配置里写的at-rule-no-unknown和scss/at-rule-no-unknown都加上 Tailwind 相关指令。这里多提一句如果你用了 Tailwind v4 的theme指令也一定记得同步加进忽略列表否则升级的时候又会被这个坑绊一次。4.6 坑六CI 里有警告但没人发现现象本地开发很顺代码提交也正常但 CI 的 lint 任务经常是黄灯团队逐渐懈怠最后 Stylelint 形同虚设。这个问题的根子在于命令出口没有卡死。默认情况下Stylelint 有 warning 时进程退出码可能还是 0如果 CI 的逻辑是“退出码非 0 则失败”这类 warning 就会被漏掉。修复方式是在模板里给所有 lint 命令加上--max-warnings 0。它的含义是只要有 warning 就直接失败和 error 同等对待。我还会在 CI 里显式设置cache复用逻辑让增量缓存在 CI 间生效避免每次都从头全量扫描。这样既严格又不会因为全量扫描拖垮构建时长。5. 模板落地后的可持续迭代建议模板写完了、CI 也通了这并不代表一劳永逸。我在实际运维中发现样式规范最容易在半年后开始“腐烂”原因是团队同学在业务压力下会想办法绕过检查。最常见的绕过方式就是在配置文件里加入大量stylelint-disable注释。针对这一点我会在模板里加一条--report-needless-disables参数它能把那些“根本没有触发对应规则错误”的 disable 注释检查出来并报错反向逼迫大家清理无效的抑制。另外一个维护思路是给模板留一个清晰的扩展点。比如后端小组可能会引入 Less前端小组可能会引入 CSS Modules这些场景不需要在根配置里一棒子打死而是通过子包覆盖或者overrides来处理。Stylelint 的 flat config 里支持overrides语法可以根据文件 pattern 应用不同的规则集这比在根配置里堆一大堆条件判断要干净得多。统一往下沉一层说Stylelint 在 Monorepo 模板里真正的价值不是“管住每一行样式”而是让样式问题成为工程问题的一部分可发现、可修复、可持续。配置做到这层它就从一个工具变成了团队规范的载体。我个人在实际操作中最深的体会是模板的优雅程度永远排在“可维护性”之后。规则不要追求多要追求每一条都能被团队理解和执行依赖不要追求全要追求每个包都加载得到。先把这套地基打稳再谈怎么加高级玩法这是我在反复踩坑之后最想提醒后来者的一句话。