
1. 为什么 Monorepo 里的样式规范比你想的更麻烦1.1 从单体应用到 MonorepoStylelint 的复杂度直接翻倍先聊一个很实际的场景。你在单个应用里配 Stylelint流程基本是固定的装包、写配置、跑一下、有报错就改半小时搞定。但一旦进入 Monorepo 架构事情就不一样了。我见过太多团队把单体仓库的 Stylelint 配置原封不动搬进 Monorepo结果不是漏检就是误报甚至出现 A 包的错误跑到 B 包的检查结果里这种离谱问题。Monorepo 的核心特征是多包共存、依赖共享、独立发布。这意味着你的样式文件分布在packages/*下不同的子项目里有的是src下的普通 CSS有的是.vue单文件里的style块还有可能是styled-components风格的 CSS-in-JS。更关键的是不同的子包可能有不同的样式约束——基础组件库要严格限制!important业务项目可能更需要控制颜色变量使用率。这就带来了一个单体应用里根本不会遇到的难题Stylelint 的配置作用域怎么划分是一套全局配置强制所有包遵守还是每个包各自维护一套如果各自维护那公共规则怎么抽取版本怎么同步这些问题的复杂度和团队规模、包数量直接挂钩包越多越需要一个清晰的规范骨架。1.2 Stylelint 在 Monorepo 里的真实定位先把概念捋清楚。Stylelint 是一个样式代码检查工具但它管得比大多数人印象中要宽得多。除了常见的color-hex-length、declaration-block-trailing-semicolon这类格式规则它还支持stylelint-order属性排序、stylelint-scssSCSS 专属规则、stylelint-config-recommended-vueVue 单文件组件样式检查等插件生态甚至可以通过自定义插件检查设计令牌Design Token的使用规范。但在 Monorepo 场景下Stylelint 的定位要更精确一些。它不只是“检查样式代码是否规范”更重要的角色是统一技术栈下的样式基线。我在搭建企业级模板时会把 Stylelint 看作三层体系第一层是格式层缩进、空格、分号、引号这些交给规则管让所有人的代码看起来像一个人写的。第二层是质量层重复属性、无效值、浏览器兼容性提示这些防止低级 bug 进入代码库。第三层是架构层CSS 变量是否被滥用、类名是否遵循 BEM、是否直接写死了颜色值。这一层才真正体现团队规范也是 Monorepo 环境下最需要统一的部分。所以如果你只把 Stylelint 当成一个“格式化工具”那格局小了。在企业级 Monorepo 模板里它是入口守卫的一部分和 ESLint、Prettier、Commitlint 共同构成工程化规范体系。2. 企业级模板的目录设计与工具选型2.1 先定 workspace 结构再谈样式规范搭建 Monorepo 模板第一步不是写 Stylelint 配置而是想清楚packages怎么分。我推荐一个经过验证的目录模型monorepo-starter/ ├── packages/ │ ├── ui/ # 基础组件库 │ ├── utils/ # 工具函数 │ ├── web-app/ # 业务应用 A │ └── admin-app/ # 业务应用 B ├── .stylelintrc.js # 根级配置 ├── stylelint.config.base.js # 公共基础配置 ├── package.json └── pnpm-workspace.yaml为什么强调先定结构因为Stylelint 的忽略配置和 overrides 都依赖目录路径。如果你后知后觉地调整包目录所有路径匹配规则都要跟着改一遍这个成本在大型仓库里很高。用 pnpm 还是 yarn 作为包管理器对 Stylelint 配置的影响不大但不要混用。混用包管理器会导致node_modules结构不一致Stylelint 的插件解析有时会出莫名问题。我习惯用 pnpm原因很简单依赖安装快、磁盘占用小更重要的是它天然的严格依赖隔离能帮你尽早暴露“幽灵依赖”问题。2.2 工具链选型标准配置 vs 自定义规则Stylelint 的配置策略我推荐“标准为主覆盖为辅”。官方维护的stylelint-config-standard是目前覆盖面最广、社区认可度最高的基础配置能解决 90% 的格式质量问题。在此基础上用stylelint-config-prettier关掉和 Prettier 冲突的格式类规则再按团队需求补自定义规则。这里有个具体建议直接给清单pnpm add -D stylelint stylelint-config-standard stylelint-config-prettier postcss postcss-html pnpm add -D stylelint-config-recommended-vue stylelint-scss如果是纯 CSS 项目postcss-html不一定要装但考虑到 Monorepo 里几乎必然有 Vue 组件我建议直接装上避免后期用到.vue文件时再补依赖。stylelint-scss是针对 SCSS 的插件如果你的组件库用了 SCSS 语法这个是必不可少的。它提供了scss/dollar-variable-pattern变量命名、scss/at-rule-no-unknownSCSS 专属指令检查等关键规则能补足标准配置无法覆盖的痛点。2.3 配置文件的划分思路企业级模板里我会把 Stylelint 配置拆成三层根级.stylelintrc.js只做两件事——引入公共配置、声明工具链信息。公共基础配置stylelint.config.base.js放所有包通用的规则包括标准规则、Prettier 冲突豁免、CSS 变量约束等。各包内部stylelint.config.js可选放业务特化规则比如电商项目禁止使用某些颜色值、后台项目强制类名前缀等。为什么不把所有规则都写进根配置因为 Monorepo 的精神是“统一基础保留差异”。你可以在根配置里用overrides按路径处理绝大部分差异但遇到某些包有极端特殊的规范时单独维护一份配置反而更清晰。提示配置文件不要用.stylelintrcJSON 格式和.stylelintrc.jsJS 格式混用。项目里最好固定一种命名避免 Stylelint 在多个文件之间按优先级合并导致预期混乱。3. 核心配置拆解每一行都有来由3.1 先看一份可用的基础配置直接上一份我实际项目里验证过的完整配置然后拆开讲为什么这么写// stylelint.config.base.js module.exports { defaultSeverity: error, extends: [ stylelint-config-standard, stylelint-config-prettier, stylelint-config-recommended-vue ], plugins: [ stylelint-scss ], customSyntax: postcss-html, overrides: [ { files: [**/*.vue], customSyntax: postcss-html }, { files: [**/*.scss], customSyntax: postcss-scss } ], rules: { // 颜色值必须小写并且尽量缩写 color-hex-case: lower, color-hex-length: short, // 禁用 ID 选择器配合组件化开发 selector-max-id: 0, // 最多嵌套 4 层避免选择器过深 max-nesting-depth: 4, // 自定义 CSS 变量命名规范必须双短横线开头且小写 custom-property-pattern: ^--[a-z][a-z0-9-]*$, // class 命名用 BEM 风格 selector-class-pattern: ^[a-z][a-zA-Z0-9]*(__[a-z][a-zA-Z0-9]*)?(--[a-z][a-zA-Z0-9]*)?$, // SCSS 变量命名统一 scss/dollar-variable-pattern: ^[a-z][a-z0-9-]*$, // 禁止 !important declaration-no-important: true }, ignoreFiles: [ **/dist/**, **/node_modules/**, **/coverage/**, **/public/** ] };3.2 关键规则的设计逻辑先看extends。stylelint-config-standard是基础它包含了color-no-invalid-hex、declaration-block-no-duplicate-properties这类通用检查基本覆盖了常见错误。stylelint-config-prettier存在的意义是关掉与 Prettier 重复的格式规则比如rule-empty-line-before。如果 Stylelint 和 Prettier 都管换行和缩进必然会出现一个改完另一个又报错的循环。再细看customSyntax。这是我在 Monorepo 里踩坑最多的配置项。.vue文件里的style块、SCSS 文件、甚至是postcss.config.js里定义的语法糖都需要对应语法的解析器。很多人只配一个customSyntax: postcss-html就以为万事大吉实际上对.scss文件还要指定postcss-scss。两者的关系不是继承而是按文件后缀匹配。然后是rules里的几条核心约束color-hex-case和color-hex-length把颜色统一为小写短格式避免#FFFFFF和#FFF混用。这条规则看起来细碎但效果立竿见影因为团队协作时不统一颜色格式代码 review 的时候每行都在吵。selector-max-id: 0直接禁用 ID 选择器。理由很简单现代前端框架大量使用组件化开发ID 选择器的特异性太高一旦被组件复用就难以覆盖几乎可以说是 CSS 的 bug 根源之一。max-nesting-depth: 4这是对 SCSS/Less 嵌套深度的限制。超过 4 层的嵌套选择器编译后的特异性会失控而且阅读代码时逻辑混乱。这条规则能强迫开发者拆分样式结构从源头避免“嵌套地狱”。custom-property-pattern和selector-class-pattern这两条是强制 BEM 和命名规范的。我在实际项目中见过太多a1、box这种无意义命名加上这条规则后至少保证团队产出的代码可以维护。declaration-no-important这条要谨慎。它在企业级模板里是合理的——如果团队没有建立“用!important前必须写注释说明原因”的规范最能落地的策略就是直接禁止。但如果你的项目里依赖第三方 UI 库做主题覆盖这条规则会频繁误报需要配合ignoreComments或except修改。建议先禁用一段时间在团队达成“尽量不靠!important解决覆盖问题”的共识后再开启。3.3 插件规则与业务场景的取舍stylelint-scss在 Monorepo 里的价值主要体现三个方面SCSS 特有语法检查、变量命名规范、以及use/forward新语法的兼容性判断。以scss/dollar-variable-pattern为例组件库和业务应用的变量命名风格应该一致。如果组件库用$primary-color业务应用用$primaryColor那主题覆盖、样式复用时就会产生语义混乱。在 Monorepo 里这种不一致会被放大——你很可能在packages/ui里定义了主题变量然后packages/web-app里引用时发现根本配不上。还要注意stylelint-order这个插件。它负责属性排序比如规定position系列必须在display系列之前。这个插件我建议在组件库里开启、在业务项目里视情况关闭。组件库的属性顺序直接影响产物 diff 的清晰度而业务项目往往需要快速迭代强制排序对效率影响较大。4. 在 Monorepo 里跑 Stylelint脚本、CI 与增量检查4.1 脚本组织方案先看根package.json里的脚本设计{ scripts: { lint:style: stylelint \packages/**/*.{css,scss,vue}\ --config .stylelintrc.js, lint:style:fix: stylelint \packages/**/*.{css,scss,vue}\ --config .stylelintrc.js --fix, lint:style:changed: pnpm --filter changed lint:style } }packages/**/*.{css,scss,vue}这种写法要注意 shell 的 glob 展开。在 zsh 里花括号展开没问题但在某些环境如 Windows 的 PowerShell里可能出问题所以我建议把路径模式用双引号包起来交给 Stylelint 忽略。pnpm --filter changed lint:style是 pnpm 的过滤语法前提是每个子包都有自己的lint:style脚本。这个组合能实现“只检查本次改动涉及的包”在大型 Monorepo 里作用很明显。4.2 增量检查从全量到“只查改动”全量 lint 在包数量少时还能接受一旦packages超过 20 个全量扫描的时间会让人崩溃。我实测过一个 30 多包规模的仓库光 Stylelint 全量扫描就需要 40 多秒这在提交前跑一次完全不能接受。增量检查的方案有几种基于 git diff只对git diff列出的文件跑 Stylelint。基于 pnpm filter只对被修改包的 lint 脚本。基于 lint-staged在 git pre-commit 阶段只 lint 暂存区文件。我推荐lint-staged作为本地钩子的方案简单可靠{ lint-staged: { *.{css,scss,vue}: [stylelint --fix, git add] } }但注意lint-staged只解决“提交时检查”的场景。CI 里如果要对整个 PR 的改动做完整检查还是需要基于 git 的增量方案思路是拿git diff --name-only --diff-filterACMR提取出新增、修改、重命名的文件再交给 Stylelint。这样既能保证检查覆盖又不会扫到dist和node_modules里的海量文件。4.3 CI 并行与缓存技巧Monorepo 的 CI 流水线里样式检查应该和单元测试、构建流程并行执行。GitHub Actions 里的典型配置name: stylelint-check on: pull_request: paths: - packages/**/*.{css,scss,vue} - .stylelintrc.js - stylelint.config.base.js jobs: stylelint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - uses: pnpm/action-setupv2 - uses: actions/setup-nodev4 with: node-version: 20 cache: pnpm - run: pnpm install - run: pnpm lint:stylepaths过滤是关键它不是性能优化而是任务触发条件优化。如果这次 PR 只改了 JS 文件完全没有必要跑 Stylelint 检查。fetch-depth: 0是为了让 git 能拿到完整 diff 记录如果后续想用增量检查这一步不能省。关于缓存Stylelint 从 13 版本开始支持--cache参数{ scripts: { lint:style: stylelint \packages/**/*.{css,scss,vue}\ --config .stylelintrc.js --cache --cache-location node_modules/.cache/.stylelintcache } }.stylelintcache文件是二进制格式不要提交到 git 里把它加进.gitignore。CI 里开启缓存的收益在第二次构建时体现——如果这次改动没有触碰样式文件整个 Stylelint 任务会秒完。5. 避坑指南我从真实项目里踩过的坑5.1 目录层级导致的误报与漏报这是 Monorepo 特有的坑。初始配置files: [**/*.{css,scss,vue}]看起来没毛病但一旦packages里嵌套了多层目录某些 Stylelint 版本会忽略深层路径。我排查过一个问题packages/ui/src/legacy/button.vue里的样式完全没被 lint 到而packages/ui/src/button.vue却正常。最终定位是因为我在根.stylelintignore里写了**/legacy/**但忽略了嵌套层级下忽略规则的优先级。正确的做法是明确配置 include而不是依赖 glob 默认行为// .stylelintrc.js module.exports { files: [packages/*/src/**/*.{css,scss,vue}], ... };同时.stylelintignore里的规则要定期审查因为目录重构时很容易出现“当年为了跳过某文件而加的忽略规则现在把整个包都忽略了”的经典事故。5.2 规则冲突Stylelint、Prettier 和 ESLint 的三角关系在我见过的团队里最常见的配置事故就是 Stylelint 和 Prettier 冲突。比如stylelint-config-standard要求declaration-block-trailing-semicolon必须存在而 Prettier 默认也给 CSS 加末尾分号两边看起来一致但当 Prettier 配置改成了semi: false冲突就来了。解法只有一个Stylelint 里显式关掉所有格式类规则交给 Prettier 处理。具体做法有两种使用stylelint-config-prettier统一关闭。手动在rules里把*-empty-line-before、*-trailing-semicolon、string-quotes这类规则全部设为null。我建议用stylelint-config-prettier因为它是持续维护的能跟随 Prettier 更新自动关闭新冲突项。手动维护的成本会越来越高。另一个容易忽略的是ESLint 对 Vue 单文件组件模板里 style 标签的检查。如果你用eslint-plugin-vue它会默认对template做检查但对style块不做处理。这时候 Stylelint 和 ESLint 不会冲突反而互补。要留意的是两个工具的ignore规则要保持一致避免出现“ESLint 跳过的文件 Stylelint 却报了错开发者不知道怎么定位”的情况。5.3 性能瓶颈一次 lint 要等 10 秒以上项目规模一大Stylelint 的性能问题就暴露了。影响性能的因素按顺序排列分别是扫描文件数量、自定义插件数量、选择器复杂度。如果遇到 lint 慢按这个顺序排查。文件数量是最大头。假设你有 100 个组件每个组件里都有样式代码全量扫描就能到 5000 个文件。这时候别犹豫直接上增量检查 --cache把每次扫描范围控制在 50 个文件以内。插件方面stylelint-scss的性能开销比stylelint-order小一个量级。后者在大型文件上会比较吃力规则多时单文件分析时间能到 200ms。如果你发现项目里 SCSS 文件普遍很大建议放弃stylelint-order因为它带来的“属性排序”收益在大型文件上会被性能损耗抵消。另外有个隐蔽的坑postcss-html 解析.vue文件时会连带着解析模板和 script 里的内容。这导致.vue文件的 lint 速度是纯 CSS 文件的 5-10 倍。规避思路是尽量让.vue文件使用外部style src./xxx.css引入而非直接内联样式但这涉及代码风格约束我一般是配合 eslint 规则一起推行而不是单纯靠 Stylelint。5.4 工程化模板的维护策略最后聊一个很容易被忽略的问题模板搭好了但“过了一个季度配置就腐烂”。Stylelint 升级频繁规则名和默认值经常会变。比较典型的是stylelint-config-standard从 34 版本到 35 版本时把alpha-value-notation的默认值从number改成了percentage很多项目的 CI 一夜之间全红。这个问题的唯一解法是不要把stylelint和stylelint-config-*直接锁定在精确版本而是在 package.json 里用^级依赖同时定期比如每季度跑一次更新查看 changelog 有没有 breaking changes。另一个维护思路是为模板加一个“自检脚本”。在 Monorepo 模板里内置一个validate:style命令检查内容包含配置文件是否合法、依赖版本是否一致、是否有被忽略的文件误入packages等。我甚至在模板里加了一个 pre-commit 钩子定时检查stylelint的版本和stylelint-config-prettier是否兼容有更新时在终端里给提示。这些自动化的小细节能让企业级模板在团队里存活更久。6. 结个尾这套模板实际给我带来了什么实话实说Stylelint 只是 Monorepo 工程化模板里的一环但它是最容易立竿见影的一环。当团队里每个人的样式代码在合并前都经过同样一套规则的校验review 的时候再也不会有人因为#FFF和#fff、类名命名风格、嵌套层级这些问题争论不休。等这套规范运行一段时间你会发现样式相关的 bug 明显变少团队新人上手时也少了很多“这个项目里为什么这么写”的疑问。我个人的建议是不要一上来就追求“规则最多最全”。先在模板里铺好stylelint-config-standard基础层把prettier冲突关掉然后根据团队实际痛点逐步添加约束。记住加一条规则很容易但每一条规则都意味着团队所有人的提交会被检查一次——规则的增量要配得上团队习惯的增量这才是企业级模板的生命力所在。如果你正在搭 Monorepo 模板或者已经搭好了但对 Stylelint 部分还不太满意希望这份拆解能帮你少踩几个坑。有什么更细的场景问题欢迎在评论区交流我尽量拿实际踩坑经验来回应。