
我在牵头搭企业级 Monorepo 工程化模板的时候最不服管的文件类型不是 JS而是样式。业务代码还有 ESLint 和 TypeScript 双层约束review 的时候可以聊类型、边界、性能轮到 CSS/SCSS除非有人愿意把八百行的样式文件逐行看完否则!important、写死的色值、毫无章法的属性顺序几乎全靠个人自觉。所以 Stylelint 是我最早写进模板计划、却也是最晚才彻底调通的一环。这篇指南就讲我在从零搭建 Monorepo 工程化模板时梳理样式规范的全过程版本选型、配置结构、多包作用域、工具链联动以及那些复制网上配置一样会翻车的坑。1. 为什么 Monorepo 里最容易被样式规范绊倒1.1 Monorepo 放大了样式文件的游离问题Monorepo 把多个包放进同一个仓库之后JS 文件会被包边界、TypeScript 配置、ESLint 规则管得服服帖帖但样式文件几乎没有类似的组织约束。每个包可以自由选择 Less、SCSS、CSS Modules、CSS-in-JS甚至同一套代码里混着好几种方案。我见过最夸张的场景组件库用的是 CSS Variables业务包又直接写color: #2d8cf0移动端包坚持用 pxWeb 端包则全面 rem结果设计师给出的同一个设计稿在仓库里要维护三套换算逻辑。这个问题的本质是Monorepo 的包边界天然鼓励局部自治而样式又不像 JS 一样有明确的模块依赖关系能被工具追踪。于是大家各自为政样式越写越多规范越来越虚。等模板搭完回头一看底层逻辑乱得没法看。Stylelint 在 Monorepo 里的第一个价值就是把这些散落在各包里的样式文件收到同一套规则之下。1.2 样式失序的典型现场我梳理过团队真实仓库里的样式问题基本逃不出这几类属性顺序混乱display和background混在一起写同一个块里position出现在最后review 根本看不出规律。值格式五花八门色值有的是#fff有的是#FFFFFF有的是rgb(255, 255, 255)一个项目里三种写法并存。选择器深度失控SCSS 嵌套动辄五六层写的人爽了改的人完全找不到具体作用于哪个元素。无效样式与重复声明同一个选择器里font-size出现两次后一条毫无察觉地覆盖前一条。单独看每一处都不至于让项目挂掉但累积起来就是样式债务。关键是这类问题在 code review 阶段几乎不可能被逐行发现效率太低而且容易引发争吵。Stylelint 的定位就是把这些原本靠人力盯的事情变成静态检查规则让机器在提交之前就把门。1.3 Stylelint 在工程化模板中的定位Stylelint 之于样式就是 ESLint 之于 JS。它可以做语法检查、规则校验、自动修复还可以和编辑器、CI、lint-staged 联动。在 Monorepo 模板里Stylelint 要解决的不只是格式对不对而是把一套跨包的统一约束注入到所有样式文件里同时保留每个包按自身场景覆盖规则的能力。这意味着我们的配置不能是某一个前端项目里的单文件.stylelintrc而是要拆成基础配置 子包覆盖的结构。基础配置定底线子包配置做差异化。后面几节我会一步步拆开讲怎么落地。2. 先把地基打稳Stylelint 版本与依赖安装的连锁反应2.1 版本矩阵Stylelint 16 之后的范式变化很多人从网上复制一份 stylelint 配置到新模板里发现规则完全不生效第一反应是自己哪里写错了。其实很可能是版本地层换了。Stylelint 14 到 15再到 16最大的变化是格式化类规则stylistic rules的逐步移除。以前大家习惯在 stylelint 里配置indentation、string-quotes、max-line-length这些排版规则到了 Stylelint 16这些规则直接从核心包删掉了。也就是说如果你用的配置是照着旧教程写的升级后这些规则会静默失效不再输出任何提示。我整理了一张版本对照表方便大家判断自己项目所处的阶段版本核心变化对模板的影响Stylelint 14弃用部分存在争议的规则老配置还能跑Stylelint 15将 stylistic 规则标记为废弃出现大量 deprecated 警告Stylelint 16彻底移除 stylistic 规则老规则失效需引入插件或交给 Prettier结论很简单新模板直接上 Stylelint 16格式化这件事交给 PrettierStylelint 专注代码质量规则。如果团队有历史习惯非要用 stylelint 做排版检查可以安装stylistic/stylelint-plugin但我会在第三章讲清楚为什么不推荐这么干。2.2 依赖安装哪些必须装、哪些按需装在 Monorepo 根目录统一安装 Stylelint 生态是最省事的方式。我用 pnpm 作为包管理器命令如下pnpm add -w -D stylelint stylelint-config-standard stylelint-config-recess-order stylelint-config-prettier这里逐个解释一下用途stylelint核心库必装。stylelint-config-standard官方标准配置包含了绝大多数代码质量规则。它是基础几乎所有项目都要用。stylelint-config-recess-order属性排序配置把display、position、box-model等按约定顺序排列。纯手工 review 太累交给它最省心。stylelint-config-prettier关闭与 Prettier 冲突的规则避免两边打架。如果项目里用了 SCSS、Less 这类预处理器还需要额外安装对应的 syntax 解析器和规则集pnpm add -w -D postcss-scss stylelint-config-standard-scss用 Vue 的话还要考虑对 SFC 里style块的支持这个我会在踩坑章节展开。2.3 pnpm workspace 下的依赖拓扑与路径问题pnpm 和 npm/yarn 最大的区别在于它默认使用隔离的node_modules结构。每个包只能访问到自己声明的依赖根目录的依赖不会被默认暴露给子包。这本来是好设计但会给 stylelint 工具链带来一个很隐蔽的困扰。假设你把 stylelint 和所有配置都装在根目录 devDependencies然后进入packages/web目录执行pnpm exec stylelint src/**/*.scss。命令本身能找到根目录的 stylelint但 stylelint 在解析插件和 shared config 时会基于当前文件的路径去查找node_modules。如果某个插件的依赖关系没有被正确提升就会出现Cannot find module stylelint-config-recess-order这类报错。我在实际模板里采用的策略是所有 stylelint 相关依赖统一放根目录所有 lint 脚本也在根目录统一编排不鼓励子包单独执行 stylelint 命令。子包只负责提供自己的配置文件执行入口始终是根目录的 scripts。这样虽然少了点包内自治的感觉但换来的是依赖关系的绝对清晰。别图省事开shamefully-hoisttrue那会破坏 pnpm 的隔离语义后续其他工具链迟早会踩坑。3. 核心配置从零写出一份能扛住生产环境的 .stylelintrc3.1 配置文件形态与 extends 顺序Stylelint 配置文件支持 JS、JSON、YAML 等格式在 Monorepo 模板里我推荐用.mjs或.cjs因为可以写注释、做动态逻辑。根目录放一份基础配置命名成stylelint.config.base.mjs各子包通过继承这份配置来获得默认规则。extends数组里的配置项是有顺序语义的后面的会覆盖前面的。所以一般把最基础的官方配置放最前面自定义的排序插件放中间任何需要兜底关闭冲突规则的放最后。下面是我模板里的基础配置// stylelint.config.base.mjs export default { extends: [ stylelint-config-standard, stylelint-config-recess-order, stylelint-config-prettier, ], // customSyntax: postcss-scss, // 多包场景不建议全局设置见踩坑章节 rules: { declaration-no-important: true, selector-max-id: 0, max-nesting-depth: [3, { ignore: [blockless-at-rules] }], unit-allowed-list: [px, rem, %, em, vh, vw, fr], color-named: never, }, ignoreFiles: [**/node_modules/**, **/dist/**, **/coverage/**], };不要小看这几条自定义规则。它们代表的是我踩过团队真实问题后沉淀下来的底线declaration-no-important禁止!important。这不是说项目里绝不能出现它而是出现时必须走例外评审而不是随手一写。selector-max-id禁止 ID 选择器。ID 选择器优先级太高后期覆盖成本极大。max-nesting-depth嵌套深度限制为 3 层防止 SCSS 写出一座金字塔。unit-allowed-list白名单单位。这样移动端和 Web 端至少不会在单位上完全失控。color-named:never禁止命名颜色强制使用十六进制或rgb方便后续做主题变量替换。3.2 规则分级error、warn、null 的企业级选择Stylelint 的每条规则都可以设成error、warn或null。很多模板一上来就把所有规则设成 error结果团队成员第一次运行 lint 看到几百个红色报错心态直接崩了。我建议按不可接受程度分级规则类型示例建议级别理由格式类indentation、string-quotesnull或交给 Prettier排版交给 Prettier别重复管语法错误类string-no-newline、function-calc-no-invaliderror写出这种基本就是 bug严重质量问题declaration-no-important、selector-max-iderror必须拦截在合入前规范约束类max-nesting-depth、unit-allowed-listwarn起步给团队一个适应期后续升 error过于主观的规则color-namedwarn或null视团队审美而定这种分级方式在生产环境非常实用。warn 不会阻塞提交但会在 CI 日志里持续出现提醒团队这里有债要还。等大家适应了新习惯再挑几个关键 warn 升级成 error整个过程平滑可控。3.3 与 Prettier 的边界为什么不能两套东西管同一件事我见过不少项目同时用 Prettier 和 Stylelint然后两边都配了缩进、引号、分号规则结果每次保存文件Prettier 改一遍Stylelint 又改一遍Git 历史里全是莫名其妙的冲突。这属于典型的职责不清。我的原则是排版归 Prettier语义归 Stylelint。Prettier 负责缩进、引号、换行、分号等排版问题Stylelint 负责重复属性、无效值、选择器复杂度、属性顺序、命名约定等格式之外的质量问题。Stylelint 16 移除了 stylistic 规则其实就是在官方层面把这个边界定死了。如果你非要通过stylistic/stylelint-plugin恢复那些格式化规则请务必确认它与 Prettier 的冲突规则已经被stylelint-config-prettier关掉否则两边打架只是时间问题。3.4 SCSS 与 CSS-in-JS 场景的规则补充处理 SCSS需要让 stylelint 能理解$variable、mixin、include这类语法否则会误报一批at-rule-no-unknown错误。两种做法第一种安装postcss-scss并在配置里加customSyntax: postcss-scss。 第二种直接用stylelint-config-standard-scss它内部已经集成了合适的 syntax 和 scss 专项规则。我更推荐第二种。因为stylelint-config-standard-scss不只是解决语法解析还内置了mixin命名规范、变量命名规范等一批 SCSS 实践规则比自己在 rules 里拼要省心得多。CSS-in-JS 又是另一回事。如果项目里用 styled-componentsstylelint 默认解析器根本不认识模板字符串里的 CSS。需要额外引入为 CSS-in-JS 设计的 syntax 插件。我的建议是如果 CSS-in-JS 只出现在个别业务包不要把它写进根配置而是用overrides按文件路径单独覆盖避免污染正常 CSS 的解析流程。这一点在 Monorepo 多包场景下尤其重要。4. Monorepo 多包场景下的忽略规则与动态作用域4.1 根配置与子包配置的继承与覆盖Stylelint 解析配置时会从被检查文件所在目录开始逐级向上查找配置。所以在 Monorepo 里你可以给每个子包单独放一份.stylelintrc让它只作用于自己的目录也可以只在根目录放一份统一配置作用于全仓库。在实际模板里我采用根配置为底座 子包覆盖的模式。子包配置写成// packages/web/stylelint.config.mjs import base from ../../stylelint.config.base.mjs; export default { ...base, rules: { ...base.rules, // Web 端允许 rem并额外要求禁止 ID 选择器 unit-allowed-list: [px, rem, %, em, vh, vw, fr], selector-max-id: 0, }, };这种方式比extends字符串更灵活因为你可以直接读取基础配置对象再做合并。有些团队习惯用extends: [../../stylelint.config.base.mjs]这在部分场景下也能工作但合并逻辑没有 JS 对象那么直观多包覆盖时容易踩extends 和 rules 同时存在的层级关系这种坑。我更推荐 export default 一个由 base 合并出来的新对象行为完全可预测。4.2 ignoreFiles 与 .stylelintignore 的分工很多人在配置里同时看到ignoreFiles和.stylelintignore却不知道两者的区别配置方式相对路径基准适用场景ignoreFiles配置文件内相对于配置文件所在目录子包内想忽略某些构建产物目录时最合适.stylelintignore独立文件相对于当前工作目录cwd全局忽略 node_modules、dist 等公共目录时更直观在 Monorepo 模板里我建议公共目录node_modules、dist、coverage用根目录.stylelintignore统一忽略而每个包自己的特殊忽略比如某个包里有自动生成的样式文件用子包配置文件里的ignoreFiles处理。这样每个忽略规则都离它对应的代码最近后续维护的人一眼就能看懂。4.3 动态 glob为什么固定路径列表会漏检早期模板我图省事在根 package.json 里写死脚本{ lint:style: stylelint \packages/web/src/**/*.css\ \packages/mobile/src/**/*.scss\ }结果可想而知新加入的packages/admin完全不被检查直到某天同事在 CI 里发现新包样式错误没拦住。问题根源就是路径列表是静态的Monorepo 的包集合是动态的。更好的做法是让命令自动匹配所有包的样式文件{ lint:style: stylelint \packages/*/src/**/*.{css,scss,vue}\ --max-warnings 0 }或者干脆让每个子包自己定义lint:style脚本根目录用pnpm -r run lint:style串联执行。第一种方式简单直接第二种方式给了子包更多自主空间。两种我都用过团队规模小选第一种规模化之后建议切到第二种。4.4 一个多包覆盖规则的实战案例讲一个真实案例。我们模板里有packages/web和packages/mobile两个业务包需求差异很大Web 端要求全面使用 rem 做响应式禁止!important。Mobile 端在一个遗留基础库里大量使用 px 和!important硬改成 error 会导致根本无法合入。所以我在基础配置里把declaration-no-important设成warn但在 Web 子包配置里覆盖成error// packages/web/stylelint.config.mjs export default { ...base, rules: { ...base.rules, declaration-no-important: error, unit-allowed-list: [rem, %, em, vh, vw, fr], }, };这样既保住了全局底线又让不同包按照自己的技术债情况渐进收敛。样式规范在企业级 Monorepo 里真不是一刀切而是统一底线 差异化覆盖的组合拳。5. 落地执行lint-staged、编辑器联动与 CI 门禁的配合5.1 lint-staged只检查暂存区别全量跑全仓跑 stylelint 在 Monorepo 里非常慢尤其包多、样式文件多的时候。我用的方案是 lint-staged只在 Git 暂存区里筛选出本次变更的样式文件提交前检查并自动修复。根目录的.lintstagedrc.json很简单{ *.{css,scss,vue}: stylelint --fix --max-warnings 0 }需要注意一个细节lint-staged 会把匹配到的文件路径追加到命令的末尾。所以上面这条命令实际上会变成stylelint --fix --max-warnings 0 file1.css file2.scss如果你的脚本里又想用通配符又想接收文件列表就会出现文件被检查两遍的尴尬情况。正确做法就是像我这样直接把文件列表交给 stylelint 处理不要在命令行里再加packages/**/*.css之类的额外通配。5.2 VSCode 保存即修settings 里的精确配置光靠 commit 前检查还不够最好让开发者在保存文件的那一刻就获得修改建议否则每次提交都要等 lint-staged 去改文件体验很割裂。VSCode 里需要注意两点一是安装 Stylelint 官方扩展二是在 settings.json 里告诉它检查哪些方言{ stylelint.enable: true, stylelint.validate: [css, scss, less, vue], editor.codeActionsOnSave: { source.fixAll.stylelint: explicit } }注意stylelint.validate里如果写了vue需要保证当前项目能够正确解析.vue文件里的style块否则扩展会报一堆解析错误。这个坑我后面会细讲。5.3 CI 门禁只对变更文件做增量检查lint-staged 解决的是本地提交体验CI 里我更推荐做一次增量检查。如果你的 CI 是基于 Git 分支的可以通过 diff 拿到本次变更涉及的样式文件然后逐一交给 stylelintgit diff --name-only --diff-filterAM origin/main...HEAD \ | grep -E \.(css|scss|sass|vue)$ \ | xargs -r pnpm exec stylelint --max-warnings 0解释几个参数--diff-filterAM只关心新增和修改的文件避免把 rename 的旧文件也拉进来检查。grep -E过滤出样式相关后缀。xargs -r表示如果没有匹配到任何文件就不要执行 stylelint避免 CI 直接报错退出。这个方案没有引入任何额外工具在 Jenkins、GitHub Actions、GitLab CI 里都能用。包多之后你也可以换成 Turborepo 或 Nx 的任务编排但原理是一样的只处理变更集。5.4 自动修复与人工 review 的边界Stylelint 的--fix能自动解决的问题机器绝不要留给人工。但有一类规则它修不了比如declaration-no-important、selector-max-id、max-nesting-depth这些需要人理解代码意图才能处理的问题。这类规则必须设成error靠--max-warnings 0把 warn 也挡在门外。我见过不少团队只靠本地--fix自动修结果很多不能自动修的问题一直躺在 warn 里没人管时间一长整个规则体系形同虚设。所以在 CI 里务必加上--max-warnings 0让所有未处理的 warn 和 error 一样阻断合入逼迫团队直面问题。6. 踩坑实录我在模板搭建中遇到的六个典型问题6.1 坑一升级 Stylelint 16 后规则静默失效现象很诡异我把网上一份 2022 年的 stylelint 配置导入新模板跑命令行没有任何报错但明显不合理的缩进和引号也没有任何提示。一开始我以为规则没加载反复检查extends路径发现配置确实生效了就是不报错。排查链路是这样的先确认 stylelint 版本16.x再确认 standard 配置版本36.x最后翻 release notes才发现 Stylelint 16 已经把 stylistic 规则全部移除了。也就是说indentation、string-quotes、max-line-length这些规则在 16 里根本不存在写了等于白写。解决方式两个选择要么安装stylistic/stylelint-plugin来恢复这些规则要么干脆放弃用 stylelint 管排版交给 Prettier。我的最终决定是交给 Prettier因为 Format 这件事 Prettier 做得更好也不存在规则维护成本。这个坑也提醒我技术方案不能只看配置教程必须先确认工具版本对应的配置生态。6.2 坑二customSyntax 设置成 postcss-scss 后.vue 里的样式无法解析为了处理 SCSS我一开始在根配置里写了customSyntax: postcss-scss。结果跑了一批.vue文件后stylelint 直接把style langscss块里的内容按 SCSS 解析遇到::before这种合法 SCSS 写法时反而报解析错误。排查后发现customSyntax是一个全局设置一旦声明所有文件都会用这个语法解析器包括普通 CSS 和.vue里的样式块。修复方式是利用overrides按文件类型分别设置export default { extends: [stylelint-config-standard, stylelint-config-recess-order], overrides: [ { files: [**/*.scss, **/*.vue], customSyntax: postcss-scss, }, ], };但这样还是不够因为.vue文件里不仅有 scss还可能有普通 css。更稳妥的方案是引入专门的 vue 语法支持比如通过stylelint-config-html/vue这类共享配置或者安装postcss-html搭配customSyntax。我在模板里最终选择的是SCSS 场景用stylelint-config-standard-scssVue 场景单独建一条overrides分支两种解析器各管一摊不再互相干扰。6.3 坑三pnpm monorepo 里找不到 stylelint 插件模块这个坑几乎每个用 pnpm 搭 Monorepo 的团队都会遇到。现象在根目录执行 stylelint 一切正常但进入某个子包执行pnpm exec stylelint src/**/*.scss报Cannot find module stylelint-config-recess-order。排查链路先确认插件确实装在根目录 devDependencies再看子包根目录的node_modules发现里面根本没有这个包。这是 pnpm 隔离 node_modules 导致的正常现象——子包不能直接访问根目录的依赖除非该依赖被声明为子包的依赖或者通过 workspace 协议显式引入。解决方式我从两个方案里选了一个最省心的统一在根目录维护所有 lint 脚本子包不单独跑 stylelint。如果你确实需要子包独立执行就把它依赖的 stylelint 插件显式加到该子包的 devDependencies 里。不要为了省事开shamefully-hoisttrue短期看似解决了问题长期会破坏 pnpm 的依赖隔离能力其他工具链迟早找上门。6.4 坑四lint-staged 在 Monorepo 中只检了部分包有一次我发现提交后有个新包的样式文件没被 lint-staged 处理但老包都正常。排查发现这个新包自己建了一份.lintstagedrc和根目录的配置并行存在。lint-staged 查找配置时会优先使用离 Git 根目录最近的配置文件最后的结果就是有的包用根配置有的包用自己的配置行为完全不可预期。解决方式Monorepo 模板里统一规定全局只允许根目录维护一份 lint-staged 配置子包一律不得自建。对于每个子包的特殊 lint 需求通过根配置里的路径过滤条件进行区分。这也让新人接手时只需要看一个入口文件心智负担会小很多。6.5 坑五CI 里只有 warning 的样式问题被直接放行这个问题不像报错那么显眼但危害很大。某次我在 CI 日志里看到 stylelint 输出了大量 warning但流水线依然是绿色通过没有任何失败。原因是 stylelint 的默认退出码在有 warning 时是 0只有 error 才会返回非 0。这意味着如果团队里有人把max-nesting-depth配成 warn那么这些超深嵌套会一直藏在 warning 里永远不会阻断合入。解决方式就是在 CI 命令里加--max-warnings 0pnpm exec stylelint packages/*/src/**/*.{css,scss} --max-warnings 0把 warning 升级为整体失败的条件。这一步做完样式规范才算真正有了硬约束。6.6 坑六VSCode 里 Stylelint 和 Prettier 同时自动修复文件被来回改最后一个是编辑器层面的坑。某位同事反馈说保存文件后样式文件像抽风一样一会儿 yarn 一会儿格式化Git diff 里出现大量无关改动。排查发现他的 settings.json 里同时配置了editor.formatOnSave和source.fixAll.stylelint而文件默认 formatter 是 Prettier。两边都要改同一个文件于是来回覆盖。解决方式是把职责彻底分开Prettier 负责整个文件的排版作为 VSCode 的默认 formatter。Stylelint 只通过 code action 修复它自己的规则不碰排版类问题。对应配置就是我在 5.2 节写的那套。只要你没有在 stylelint 里启用 stylistic 插件就一定不要让它去格式化代码只保留source.fixAll.stylelint的语义修复即可。这也是为什么我一直强调排版归 Prettier语义归 Stylelint——这不是理论洁癖是实际操作中的教训。7. 模板跑通后的复盘与后续扩展7.1 模板里和样式规范相关的目录结构最终这套模板里和样式规范相关的部分长这样. ├── stylelint.config.base.mjs # 根级基础配置 ├── .stylelintignore # 全局忽略公共目录 ├── .lintstagedrc.json # 提交前增量检查 ├── packages/ │ ├── web/ │ │ └── stylelint.config.mjs # 继承 base 并覆盖 Web 规则 │ ├── mobile/ │ │ └── stylelint.config.mjs # 继承 base 并覆盖 Mobile 规则 │ └── components/ │ ├── src/ │ └── stylelint.config.mjs └── scripts/ └── lint-style.sh # CI 增量检查脚本这个结构的核心思路是一份底座多份差异化配置一个统一执行入口。底座定死底线差异部分通过 JS 对象合并实现执行入口放在根目录让所有工具链共用同一份依赖。7.2 从 Stylelint 到完整工程化底座接下来补什么Stylelint 只是 Monorepo 工程化模板里的一环。我在跑通样式规范之后紧接着补了这几层commitlint统一提交信息规范配合 lint-staged 在 commit 阶段校验。changesets管理多包版本发布样式规则变更也能随版本记录。Turborepo或Nx做任务编排让 stylelint、eslint、build、test 支持增量缓存。设计令牌Design Token规范样式里不写死色值和间距统一从 tokens 文件引用这是摆脱散落色值的终极方案。每一层推进时我都沿用同一个思路基础设施在根目录统一维护业务差异在子包通过配置覆盖。这套方法论在样式规范验证过之后迁移到其他工具链一样适用。7.3 我对企业级模板的取舍心得说了这么多最后分享一点个人体会。搭建企业级 Monorepo 工程化模板时最忌讳的就是规则至上。规则设得越多、越严初期阻力就越大最后往往变成开发者和 CI 的猫鼠游戏。我在模板里刻意留了一个渐进策略新规则先设成 warn给团队一两个迭代的适应期等大家不再踩线了再升成 error 阻断合入。这套策略比一步到位温和得多效果却好得多。Stylelint 的底层价值从来不是让人难受而是把样式层面的审美分歧变成机器判断帮团队省掉无休止的 review 争论。如果你也在搭 Monorepo 模板建议先把自己团队最痛的三五条样式问题固化成规则跑通这套链路之后再慢慢补充规则会顺手很多。