Monorepo样式规范实战:Stylelint配置与避坑指南
做企业级 Monorepo 模板时多数团队会把重心压在 ESLint、TypeScript、构建链路和缓存加速上Stylelint 往往是被顺带装上的那个。但在我经手过的几个多包仓库里真正让代码 review 变得痛苦、让样式维护成本飙升的恰恰是这些看起来“差不多能用就行”的样式检查环节。CSS、SCSS、Less、甚至 CSS-in-JS 散落在十几个 package 里每个人写变量的习惯不同颜色值一会儿十六进制一会儿 rgbz-index 随手编媒体查询的断点也越写越散——这些都很难通过 code review 人肉兜住。这篇是“从零搭建企业级 Monorepo 工程化模板”系列里的第五篇专门讲 Stylelint 样式规范和避坑。我会把这套模板里实际用到的 Stylelint 配置、设计思路、以及我踩过的坑完整写出来。适合正在搭 Monorepo 工程化模板的团队负责人、前端架构师也适合想把手头项目的样式规范认真管起来的一线开发。相信看完之后你能直接把这套方案拿回去复用。1. 为什么 Monorepo 里的样式规范会变成隐形雷区1.1 样式文件比 JavaScript 更容易失控JavaScript 有 TypeScript 类型系统兜底有 ESLint 规则约束大家不觉得乱。样式文件则不同CSS 本身没有“类型”写错了也不报错最多就是页面看起来不对。更麻烦的是Monorepo 给了多个业务包、组件库、设计系统在同一个仓库里共存的土壤样式文件的管理维度一下子复杂了太多。常见失控场景包括同一套设计变量颜色、间距、字体、圆角在不同 package 里各写一份值还不一样。样式嵌套裸奔动辄七八层选择器权重高到改都不敢改。颜色混用有人用#409EFF有人用rgb(64, 158, 255)有人直接用blue。z-index从 1 到 9999 随机出现弹窗和抽屉到底谁在上面纯靠运气。无效样式、重复声明、!important到处插。这些问题单靠约定完全拉不住。Stylelint 的价值就在于把“要不要准守约定”这件事从人治变成法治而且它是可配置、可分区、可自动修复的天然适合工程化模板这种“一套配置管全仓”的场景。1.2 为什么只靠 Prettier 不够很多人会有疑问团队不是有 Prettier 吗格式化样式文件不就行了这两者解决的问题不一样。Prettier 解决的是“长什么样”比如缩进、引号、分号、换行。Stylelint 解决的是“写得好不好”比如属性顺序、颜色格式、选择器深度、命名规范、废弃属性这些是 Prettier 根本不会管的。你可以把 Prettier 理解成排版工人把 Stylelint 理解成质检员。排版工人让文件看起来赏心悦目但文件里如果用了已经废弃的属性和离谱的嵌套层级排版工人是不会关心的。所以在企业级模板里我通常是两个都上Prettier 负责格式Stylelint 负责“语义和规范”。1.3 Stylelint 在工程化链路中的定位在 Monorepo 模板里Stylelint 和 ESLint 处于平级位置。ESLint 管 JavaScript/TypeScriptStylelint 管样式文件。但 Stylelint 的灵活性在于它不止能查 CSS通过自定义语法它可以解析 SCSS、Less、Vue SFC 里的style块、HTML 里的内联样式甚至 CSS-in-JS。这意味着你可以在根目录放一份统一的 Stylelint 配置文件然后通过overrides对不同 package、不同文件类型分别开规则。这个思路正好是 Monorepo 架构下最舒服的工程化形态全局有全局的规则底线局部有局部的定制空间。2. Stylelint 版本选型和核心概念别一上来就踩坑2.1 大版本差异14、15、16 怎么选Stylelint 的迭代速度这两年不算快但每个大版本都有动作选型之前先弄清楚Stylelint 14这是最稳妥的版本很多老项目的锁定版本。支持自定义语法非标准 CSS 需要customSyntax配置。Stylelint 15默认启用了reportUnscopables等若干新默认行为对比较老的插件兼容性开始收紧。Stylelint 16是当前最有代表性的版本。它把stylistic类规则比如缩进、引号、冒号空格等全部从核心中移除移到了stylistic/stylelint-plugin。这是个大变化。如果你沿用旧配置里的indentation、string-quotes在 16 下面会直接报“规则已废弃”。在公司新搭模板我建议直接用 16。原因很直接16 的 Node 版本要求已经提高到 18.12.0 以上新仓库不会卡这个问题其次样式规范里格式相关的部分交给 Prettier 就够了Stylelint 16 把格式类规则剥离反而职责更清晰配置更轻。2.2 核心配置文件组成一个完整的 Stylelint 配置通常由这几块组成extends继承共享配置比如stylelint-config-standard、stylelint-config-recommended-scss。plugins加载附加插件比如stylelint-order、stylelint-scss。customSyntax指定自定义语法用于解析 SCSS、Less、Vue 等非标准 CSS。rules具体规则覆盖优先级最高。overrides按文件 glob 匹配做分区差异化配置。一个最小可用的.stylelintrc.json大概是这样的{ extends: [stylelint-config-standard], rules: { color-hex-length: short, declaration-block-no-duplicate-properties: true }, ignoreFiles: [dist/**, node_modules/**] }这里extends是继承意味着你可以不用从零写规则而是在社区成熟规则集基础上只覆盖本团队的差异项。这是最省力、最容易维护的姿势。千万别上来就自己编一两百条规则。2.3 理解规则分类Stylelint 16 核心里的规则可以粗略分成几类避免错误类比如color-no-invalid-hex、declaration-block-no-duplicate-properties这类规则应该无条件开启它直接拦掉语法和低级错误。限制语言特性类比如max-nesting-depth、selector-max-id、shorthand-property-no-redundant-values这类规则是为了引导书写风格防止代码腐化。stylistic 格式类16 已移除交给stylistic/stylelint-plugin或 Prettier。理解分类后你就能判断哪些规则要开哪些规则还得看团队现状。不要追求规则数量多追求的是命中痛点。3. Monorepo 下 Stylelint 的分散配置与全局统一策略3.1 根级配置与包级配置的继承关系Monorepo 模板里最忌讳的是每个 package 自己放一份完整 Stylelint 配置。那样做表面上是“独立”实际上规则很快就漂移得各不相同。到了后面A 包和 B 包的样式规范完全是两套约束效率为零。正确做法是把“基线配置”放在根目录. ├── package.json ├── .stylelintrc.json └── packages ├── components ├── utils └── app-1根目录的.stylelintrc.json负责全仓的默认规则。如果某个 package 确实有特殊需要可以在packages/xxx/.stylelintrc.json里只写差异部分Stylelint 会从当前目录往上找最近的配置并且会自动继承根级的extends和rules。举个例子一个专门做设计令牌的packages/tokens包可能只输出几十个 CSS 自定义属性你可以在这个包里的配置中额外要求属性排序{ extends: [../../.stylelintrc.json], plugins: [stylelint-order], rules: { order/properties-order: [ custom-property, display, position ] } }这里的关键是extends指向根配置保证上下层级能够叠加而不是互斥。3.2 用 overrides 处理不同语言和文件类型Monorepo 里样式文件的形态往往是混合的。组件库可能用 SCSS后台业务可能用 Less某些老的包还在用纯 CSS。如果你只有一个全局规则集直接用stylelint-config-standard它会默认按标准 CSS 解析遇到 SCSS 的mixin、include就会报错。这时候就要靠overrides按文件类型分流{ extends: [stylelint-config-standard], overrides: [ { files: [**/*.scss], customSyntax: postcss-scss, extends: [stylelint-config-standard-scss] }, { files: [**/*.less], customSyntax: postcss-less, rules: { selector-class-pattern: ^[a-z][a-z0-9-]*$ } }, { files: [**/*.vue], customSyntax: postcss-html, extends: [stylelint-config-standard] } ] }overrides的妙处在于不同文件类型可以各自叠加不同的 extends 和 customSyntax互不干扰。这也是 Monorepo 工程化模板里最值得花时间设计的部分。3.3 package.json script 设计全量检查、fix、增量检查模板能不能被团队“顺手用上”脚本命名和体验非常关键。我通常在根目录的package.json里暴露这几个命令{ scripts: { lint:style: stylelint \packages/**/*.{css,scss,less,vue}\ --allow-empty-input, lint:style:fix: stylelint \packages/**/*.{css,scss,less,vue}\ --fix --allow-empty-input, lint:style:changed: lint-staged --config lint-staged.style.config.js } }--allow-empty-input这个参数容易被忽略但很重要。仓库里如果没有匹配到样式文件时Stylelint 默认会返回非零退出码CI 就会报错。在像 Monorepo 这种包数量多、正在初期迁移的仓库里不可能保证每条分支上都有样式文件所以这个参数建议直接写上。3.4 结合 lint-staged 做增量检查全量检查适合 CI本地开发里跑全量速度慢、体验差。所以在模板里我另外单独配了一套 lint-staged 的配置专门给 git 暂存区的样式文件做检查// lint-staged.style.config.js module.exports { *.{css,scss,less,vue}: [ stylelint --fix --allow-empty-input, prettier --write ] }这套组合的好处是开发者在提交前只检查自己改动的文件速度飞快同时先stylelint --fix再prettier --write避免修复完格式又被 Prettier 改动带来的二次 diff。顺序千万别反。实测下来这个顺序是最稳的。4. 常用规则集与自定义规则的取舍既要规范又别把人逼疯4.1 stylelint-config-standard 与 stylelint-config-recommended 怎么选社区里最常见的两个共享配置是stylelint-config-recommended和stylelint-config-standard。两者的区别是stylelint-config-recommended只包含“避免错误”的规则保守、噪音小。stylelint-config-standard在 recommended 基础上还包含一部分“限制语言特性”的规则比如color-hex-length、declaration-block-no-redundant-longhand-properties风格更严。我的建议是企业级模板直接上 standard。因为 therecommended 太松很多本该拦住的写法漏过去团队很快就对 lint 失去信心。standard 也不会像某些极客配置那样让人寸步难行它卡的是底线。如果项目里用了 SCSS那就装一组配套的npm i -D stylelint-config-standard-scss注意stylelint-config-standard-scss已经内部包含了stylelint-config-recommended-scss的逻辑所以直接 extends 它就行。4.2 值得加的自定义规则组合在 standard 基础上我一般会加下面几条自定义规则它们能解决我在真实项目里遇到的痛点{ rules: { color-named: never, color-no-invalid-hex: true, declaration-block-no-duplicate-custom-properties: true, font-family-no-missing-generic-family-keyword: true, length-zero-no-unit: true, max-nesting-depth: 3, selector-max-id: 0, unit-allowed-list: [px, %, em, rem, s, ms, vw, vh, vmin, vmax, deg] } }逐条拆解一下color-named: never强制不用颜色关键字像red、blue这类全部拦截统一用十六进制。这能逼着团队使用设计系统里的颜色值而不会随手一个red。color-no-invalid-hex拦截非法十六进制色值比如#fff少写一位。declaration-block-no-duplicate-custom-properties拦截重复的 CSS 自定义属性这是我在组件库项目里遇到最多的错误变量一旦重名后面覆盖前面找 bug 找半天。max-nesting-depth: 3SCSS 嵌套层级最多 3 层。这个值可以根据团队水平调整但一旦超过 4 层选择器权重和排查难度都会暴涨。selector-max-id: 0彻底禁止 ID 选择器。理由很简单ID 权重太高组件复用时很难被覆盖。unit-allowed-list白名单单位像pt、cm、pc这类物理单位在 Web 场景下基本不会用直接禁掉。一个提醒自定义规则尽量从痛点出发。如果开的规则团队天天手动忽略那就说明这条规则定得不合理要嘛改规则要嘛改写法而不是靠禁用注释堆山。4.3 忽略文件与禁用注释的规范用法不是所有文件都应该被检查。比如构建产物、第三方样式、自动生成的 tailwind 输出文件这些都需要忽略。统一写在配置文件的ignoreFiles里{ ignoreFiles: [ **/dist/**, **/node_modules/**, **/*.min.css, packages/app-1/src/assets/styles/vendor/** ] }同时线上代码里难免会有个别必须要打破规则的场景。Stylelint 提供两种注释/* stylelint-disable-next-line declaration-no-important */ .override { color: #fff !important; }/* stylelint-disable selector-max-id */ #root { height: 100%; } /* stylelint-enable selector-max-id */但这里要强调一个底线禁用注释本身也要可审计。我见过一个大仓库里禁用注释多达两百多处每处都是前人的“坑”。后来我们约定禁用注释必须紧跟着一条说明文字不然不行/* stylelint-disable-next-line color-named -- 这个颜色是第三方地图SDK规定的不能用十六进制替代 */ .marker { color: blue; }这种写法在 Stylelint 的禁用注释里是支持的而且当别人 review 时也清楚你为什么要破例。4.4 样式变量的强制命名规范Monorepo 里最容易被 Stylelint 抓住的痛点是自定义属性命名。设计系统的 CSS 变量如果叫--red、--blue这种口语化名字几期迭代后必然被推翻。我们模板里用stylelint-declaration-block-no-ignored-properties配合自定义正则要求自定义属性必须符合语义化模式{ rules: { custom-property-pattern: ^([a-z][a-z0-9]*)(-[a-z0-9])*$ } }这条规则看上去简单但实际效果是团队不敢再写--col-primary这种“看上去没问题实际上没有层级”的名字而是会写--color-brand-primary、--spacing-page-margin这种可读性强的变量名。命名这件事靠约定靠不住靠 lint 反而稳。5. 与编辑器、CI 的联动让规范“无处不在”又不烦人5.1 VS Code 配置保存时自动修复的坑光有命令行检查不够开发者的日常是在编辑器里写代码。VS Code 下需要安装官方插件stylelint然后在.vscode/settings.json里配置{ stylelint.validate: [css, scss, less, vue], editor.codeActionsOnSave: { source.fixAll.stylelint: explicit } }这里有几个坑要重点说老的 VSCode Stylelint 插件默认只生效在 css 文件上scss、less、vue 需要通过stylelint.validate显式声明否则写完 SCSS 完全不报错。如果项目里同时配置了prettier.prettierPath和 Stylelint 的格式规则两者可能在保存时打架。我的解决思路是所有格式类规则全部交给 PrettierStylelint 只做语义检查这样保存动作是单线程的不会出现“先格式一下、再格式化回去”的问题。在工作区里如果某个 package 有自己的配置VS Code 插件也会自动往上找级联行为与命令行一致不需要额外配置。5.2 CI 阶段全量检查和增量检查配合使用CI 里的 lint 策略我分成两类主分支保护对main/master跑全量lint:style确保合并后仓库整体是干净的。PR 阶段对merge request的改动文件跑增量检查方式是在 CI 里先获取改动列表再 filter 出样式文件喂给stylelint。增量检查命令可以用git diff拿文件列表git diff --name-only --diff-filterACMR origin/main...HEAD \ | grep -E \.(css|scss|less|vue)$ \ | xargs stylelint --allow-empty-input这样做最直接的好处是仓库早期的历史样式可以先不清零不会因为历史债太多导致每条 PR 连门都出不去。等团队慢慢把存量文件迭代干净再逐步扩大到全量。5.3 让 lint 结果可读性更好CI 中 stylelint 默认输出是带颜色和堆栈信息的到了 Jenkins/GitHub Actions 日志里经常被截断。可以在命令行加上输出格式stylelint packages/**/*.{css,scss} --formatter stylish或者输出成 JSON喂给后续的代码质量平台做聚合展示stylelint packages/**/*.{css,scss} --formatter json stylelint-report.json5.4 提速技巧开启 stylelint cache在团队规模变大以后全量 Stylelint 也会变慢。Stylelint 支持--cachestylelint packages/**/*.{css,scss} --cache --cache-location node_modules/.cache/.stylelintcache缓存文件放node_modules/.cache下有两个好处一是不污染仓库二是 CI 可以在缓存策略里直接保留node_modules/.cache加速。实测下来几百个样式文件的全量检查热启动能快一半以上。6. 常见问题与排查技巧实录我踩过的那些 Stylelint 的坑6.1 SCSS 文件自定义语法报错最典型的错误TypeError: Expected a valid CSS selector原因几乎都是没有给 SCSS 文件指定customSyntax: postcss-scss而规则集本身要求标准 CSS 解析。解决方式前面已经说过overrides里按.scss后缀指定 customSyntax。但还有一个衍生坑如果你同时用了stylelint-config-standard-scss实际上这个共享配置并不包含 customSyntax。很多人在网上找到的资料里说“用 config-standard-scss 就不需要配 syntax 了”这是误导。customSyntax 必须你自己配或者在.stylelintrc的overrides里写清楚。6.2 Vue 文件下大括号报错在 Vue SFC 里style langscss如果被普通 CSS 规则集解析会出现一组诡异报错。比如Unexpected unknown at-rule use (at-rule-no-unknown)排查逻辑是at-rule-no-unknown这条规则不认识 SCSS 的use、mixin、include。在 Vue 文件里Stylelint 走的是postcss-htmlpostcss-scss联合语法。配置方式要稍微绕一下{ overrides: [ { files: [**/*.vue], customSyntax: postcss-html, extends: [stylelint-config-standard, stylelint-config-standard-scss], rules: { at-rule-no-unknown: [ true, { ignoreAtRules: [use, mixin, include, forward, function, return, extend, if, else, each, for, while, at-root] } ] } } ] }核心思想是对 Vue 文件先用postcss-html拆出style块再交给后面的 SCSS 解析逻辑处理。但这里不同规则集叠加以后SCSS 的 at-rule 依然可能漏判所以最稳的办法是手动把 SCSS 的 at-rule 全部加入忽略列表。6.3 CSS-in-JS 的检查方案如果是 styled-components 或者 EmotionStylelint 也能检查但要装stylelint-config-styled-components和postcss-styled-components然后配置{ overrides: [ { files: [**/*.{ts,tsx}], customSyntax: postcss-styled-components, extends: [stylelint-config-styled-components] } ] }不过说实话在实际的企业级仓库里CSS-in-JS 的 stylelint 收益相比文件型 CSS 会低一些。样式标签写到 JSX 里规则错误通过 TypeScript 类型也能拦掉一部分。如果团队没有强烈需求我不建议在模板第一期就引入 CSS-in-JS 的 stylelint 检查先跑通纯 CSS/SCSS 才是性价比最高的路径。6.4 升级 Stylelint 16 后规则全部被标废弃很多团队从 15 升到 16一跑全量满屏都是规则废弃提示。这并不代表你的配置坏了而是因为 stylistic 规则被移出核心。处理方式有两种一是彻底向 Prettier 移交格式责任把所有indentation、string-quotes、declaration-block-trailing-semicolon这类规则删掉让 Prettier 统一管格式。这是我最推荐的方式。二是装stylistic/stylelint-plugin继续用 Stylelint 管格式。但我觉得这失去了 16 剥离格式规则的初衷会增加两套工具在格式上的冲突面。企业级模板里越简单的规则边界越稳。6.5 报错信息定位不准怎么调试排查 Stylelint 问题有几个命令和参数非常有用打印配置详情npx stylelint --print-config packages/app-1/src/index.scss看看某个文件最终应用了哪些规则继承和 overrides 是否生效。调试语法解析npx stylelint --stdin-filename packages/app-1/src/index.scss input.scss快速验证一段样式内容在当前配置下会报什么错。找到无效/冗余配置npx stylelint --report-needless-disables可以把代码里其实没起作用的禁用注释捞出来方便清理。开启 debug 模式DEBUGstylelint:* npx stylelint ...能看到解析器加载、规则初始化等内部信息排查插件冲突时特别好用。7. 常见问题速查表症状原因解决方案SCSS 文件大量 at-rule 报错没有配置postcss-scss在overrides中对*.scss设置customSyntaxVue 文件style langscss报 use/mixin 错误postcss-html 未与 scss 语法正确组合将 SCSS at-rule 加入ignoreAtRules升级 16 后全部规则被标废弃stylistic 规则已移出核心删除格式类规则交给 Prettier长时间全量 lint 变慢缓存未开启加--cache --cache-location空仓库/空分支 lint 失败没有匹配到任意样式文件脚本加--allow-empty-input规则与 Prettier 冲突两套工具都管格式约定 Stylelint 只做语义格式全给 Prettier子 package 规则漂移包内独立配置过多统一走根配置 extends包只写差异项这个表是我每次去客户现场处理 Monorepo 工程化问题时常用的速查思路。大部分问题不是单个配置写错了而是层级覆盖和工具职责边界没有理清楚。8. 一点我的个人体会Stylelint 这套东西真正决定它能不能活下去的不是技术难度而是规则集和团队流程能不能咬合。你在模板里写了二十条激进的规则第一天就被人集体 bypass那就等于没有规范。我自己的实践经验是分阶段推进第一阶段只开防错类规则让团队没有痛感第二阶段引入命名和嵌套限制开始统一设计变量第三阶段再上自定义属性和排序等高级约束。如果你正在搭类似的企业级 Monorepo 工程化模板可以先把这篇里的根配置和 overrides 结构抄回去试试。等跑通了再把 lint-staged 和 CI 增量检查接上最后再看团队反馈逐步收紧规则。样式规范这事做得早不如做得稳做得稳不如做得能让团队一直用下去。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →