ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Monorepo 下 Stylelint 样式规范工程化实践与配置方案

Monorepo 下 Stylelint 样式规范工程化实践与配置方案 如果把 Monorepo 工程化模板的搭建比作装修一套房子那包管理、构建、发布这些就是水电和墙体而样式规范更像是墙面的漆——住进去天天看得见出了问题也最难收拾。最近我在整理团队的新模板核心任务之一就是把 Stylelint 这套样式检查体系从原来的单仓库平滑迁移到基于 pnpm workspace 的 Monorepo 架构里。单仓库时代Stylelint 的配置就是根目录一个文件、几条 script 的事真到了 Monorepo 场景才发现里面藏着不少平时根本不会注意到的坑。这篇文章不打算从零讲 Stylelint 是什么重点记录我在模板搭建过程中的完整思路、配置方案、工具版本取舍以及最终沉淀下来的一套可以在多包仓库里直接落地的实践。如果你正在做 Monorepo 工程化或者正想把团队的样式规范从“口头约定”升级成“自动检查”这篇应该能帮你少走不少弯路。1. Monorepo 里做样式规范为什么和单仓库完全不是一回事1.1 单仓库的 Stylelint 方案放到 Monorepo 里哪里不对劲在单仓库里做样式规范流程非常简单项目根目录装一个 stylelint配上 .stylelintrc.jsonpackage.json 里加一条 “lint:style”: “stylelint src/**/*.{css,scss,less}” 的 script完事。提交前跑一遍CI 里再跑一遍基本够用。但这个方案在 Monorepo 架构里会遇到几个比较棘手的现实问题。首先检查对象的数量级完全不同。单体项目再怎么大样式文件也就几百个Monorepo 里随便就是十几个甚至几十个包每个包都有自己的样式文件。全量扫描一次耗时从原来的几秒变成几十秒甚至更久这个开销在本地开发时是没法接受的。其次不同包的技术栈很可能不一样有的包用 SCSS有的包用的是 Tailwind CSS还有的包可能是 Less 或者纯 CSS Modules。如果给所有包套同一套配置要么因为自定义语法解析不了直接报错要么规则根本不适用于某些技术栈误报满天飞。还有一个容易被忽略的点Monorepo 里每个包可能都有自己的构建体系如果配置和依赖没有统一管理很容易出现不同包之间 Stylelint 版本不一致、检查结果对不上的问题到时候规范就形同虚设了。所以我说单仓库的那套做法搬到 Monorepo 里不是“能不能用”的问题而是“适不适用”的问题。真正的 Monorepo 方案需要同时解决配置共享、技术栈差异化、执行粒度和性能这四件事。1.2 先想清楚规范边界统一到什么程度才算统一很多团队搭 Monorepo 模板时第一个念头就是“所有包用同一套规则”这个想法本身没问题但容易矫枉过正。我在实际项目中看到的普遍情况是同一个 Monorepo 里核心业务包和工具函数包的技术栈、开发周期、维护人员完全不同强制它们遵循完全一致的样式规则只会带来无休止的规则适配和配置 override最后搞出来的配置比业务代码还复杂。更合理的思路是“基础统一 技术栈扩展 业务自定义”三层结构。最底层是基础规则也就是所有包都必须遵守的通用规则比如禁止无效属性、禁止重复属性、统一缩进和引号风格中间层是技术栈扩展针对 SCSS、Less、Tailwind 分别引入对应的语法解析器和规则集最上层才是业务自定义留出空间给特定包去覆盖个别规则。这样分层之后配置的维护成本会低很多。新增一个包时只需要判断它属于哪种技术栈然后套对应的那套配置就行不需要重新理解整套规则。说实话模板要能达到这个效果才配得上“工程化”三个字。2. 搭建前的准备工具链选型和版本取舍2.1 Stylelint 15 和 16 怎么选版本升级到底改了什么搞 Monorepo 模板有一个潜规则能选新版本就选新版本因为模板的寿命往往比预期长。但 Stylelint 16 这个升级有点特殊它不是简单加了一些规则而是直接移除了所有 stylistic风格类规则比如缩进、引号、大小写这类跟代码格式化有关的规则全部不再内置。官方的说法是这些职责应该交给 PrettierStylelint 只专注做“代码质量”层面的检查。这就带来一个连锁反应如果你从 15 升级到 16原本依赖 stylelint-config-prettier 来关闭冲突规则的做法在 16 里基本没用了因为风格类规则已经被移除了自然也就不存在冲突。同时16 对 Node 版本有硬性要求需要 18.12.0 以上。如果你团队里还有人在用老版本 Node这里就得提前确认。此外16 默认不再内置对 Less 和 SCSS 的解析支持需要额外通过 customSyntax 指定 postcss-less 或 postcss-scss否则直接解析会报 CssSyntaxError。我的建议是如果是从零搭新模板直接上 Stylelint 16配合 Prettier 做格式化职责划分清晰如果是在存量项目里升级一定要先跑一遍全量扫描看看有多少规则因为版本升级被移除或改变了行为不要盲目升级。下面这个表是我在迁移时整理的差异基本覆盖了主要影响点。对比项Stylelint 15Stylelint 16风格类规则缩进、引号等内置需用 stylelint-config-prettier 消除与 Prettier 冲突移除格式交给 PrettierSCSS/Less 解析部分内置需要 customSyntax 显式指定Node 版本要求14 以上18.12.0 以上默认规则集stylelint-config-standard 15.xstylelint-config-standard 36.x规则有增减性能正常优化了缓存和解析大仓扫描更快2.2 配套插件选型哪些值得装在模板里哪些可以缓一缓Stylelint 本身只是一个框架真正干活的是规则和插件。我最终在模板里保留了这几个不是拍脑袋选的每个都有明确的使用场景。第一个是 stylelint-config-standard这是官方标准配置覆盖了绝大多数通用规则包括禁止无效颜色、无效字体、重复选择器、未知属性等等是整套配置的地基。凡是自定义规则没有覆盖到的地方都用它兜底。第二个是 stylelint-config-standard-scss这个只能在 SCSS 文件上用它会在 standard 的基础上增加针对 SCSS 特有语法和特性的规则比如 mixin 和 include 的写法、变量定义的检查等。第三个是 stylelint-config-clean-order它解决了属性书写顺序的问题。样式属性顺序在团队协作里其实非常容易引发 diff 冲突两个人同时改一个文件一个把 color 写前面另一个写后面合并时就看不出到底改了哪一行。设置好属性顺序后配合 --fix 自动排序这类无谓冲突基本可以消除。第四个是 postcss-scss它不是 Stylelint 插件而是 postcss 的语法解析器给 Stylelint 16 解析 SCSS 文件用的。第五个是 stylelint-config-tailwindcss如果包里有 Tailwind CSS这个插件会用来处理 tailwind 和 apply 这些 at-rule否则默认规则会把它们当成未知 at-rule 报错。有一类插件我没有在模板里默认启用那就是 stylelint-declaration-strict-value它用来强制属性必须使用 CSS 变量而不能写死值。这个想法很好但对业务包来说太严格了会导致大量误报我把它放在了“可选扩展”的位置哪个包需要哪个包自己开。给模板做依赖选型时原则应该是“默认配置要少而稳扩展配置要广而清晰”尽量让基础模板开箱即用而不是预装一堆规则让团队天天跟 lint 报错斗争。2.3 依赖装在哪里根目录还是各个包这是一个 Monorepo 里特别容易忽略的细节。Stylelint 的配置文件有向上查找机制会从被检查文件所在目录开始逐级向上找最近的 .stylelintrc。所以在 Monorepo 的根目录放一个配置文件子包不单独配置也能跑起来。但这只解决了配置问题依赖本身装在哪里同样关键。我见过有的团队在每个包里分别安装 stylelint 和各类插件版本没有锁定结果一段时间后各个包的 Stylelint 版本出现了轻微差异同一套规则检查结果却不一样很难排查。正确做法是在 pnpm workspace 模式下把 stylelint 和所有插件作为 devDependencies 统一安装到根目录同时通过 pnpm 的 shamefully-hoist 或默认的依赖提升机制确保每个包都能解析到这些命令。子包只需要在 package.json 里写好 lint:style 的 script不用重复安装。这背后的逻辑和 Monorepo 的依赖管理思路是一致的公共的工具链尽量收敛到根目录统一管理才能真正锁版本、锁行为。子包如果要装业务依赖那是另一回事不该和工具链混在一起。3. 配置文件怎么写一套配置管理多种技术栈3.1 根目录配置 包级覆盖overrides 是核心机制Stylelint 在 Monorepo 场景下最实用的能力就是 overrides。它允许你针对不同文件类型应用不同的配置解决不同技术栈共存的问题。我推荐的模板结构是根目录一套基础配置各个子包不写自己的 .stylelintrc除非有个性化需求所有差异全部通过根配置的 overrides 来区分。基础配置大概长这样我直接贴出来{ extends: [stylelint-config-standard], plugins: [stylelint-config-clean-order], rules: { order/properties-order: null, color-named: never, declaration-block-no-duplicate-properties: true, max-nesting-depth: 3, selector-class-pattern: ^[a-z][a-zA-Z0-9]*$ }, ignoreFiles: [**/dist/**, **/node_modules/**, **/coverage/**] }这里解释几个点。color-named 设为 never是强制颜色值必须使用十六进制或 rgb/hsl不允许直接写 red、blue 这种颜色名称统一风格后更容易维护主题。max-nesting-depth 限制嵌套层数不超过 3 层这对 SCSS 尤其重要嵌套太深基本意味着 HTML 结构也有问题。selector-class-pattern 是我加的有一点“偏执”的规则它强制类名必须是小写字母开头的驼峰格式这么做是为了和 JavaScript 变量命名方式保持一致方便在 React 组件里做 CSS Modules 映射时一一对应。“order/properties-order”: null 这个要重点说一下。因为 stylelint-config-clean-order 插件本身已经包含了一套属性排序规则它提供的是一套社区公认合理的顺序。如果你再在 rules 里自定义一份 order/properties-order就会出现两套规则同时生效导致排序逻辑冲突。所以这里显式置空让插件的默认规则完全生效不要在外部覆写。3.2 SCSS、Tailwind、CSS Modules 共存的三段式配置光有基础配置不够还得解决不同技术栈的解析问题。我在 overrides 里配置了三种常见的文件场景SCSS、Tailwind、CSS Modules。这三个场景基本覆盖了当前前端团队的主流技术栈。{ overrides: [ { files: [**/*.scss], customSyntax: postcss-scss, extends: [stylelint-config-standard-scss, stylelint-config-clean-order] }, { files: [**/*.module.css], rules: { selector-class-pattern: null } }, { files: [**/*.css], rules: { at-rule-no-unknown: [ true, { ignoreAtRules: [tailwind, apply, variants, responsive, screen] } ] } } ] }对于 SCSS 文件核心是 customSyntax 指定为 postcss-scss否则 Stylelint 16 无法解析 SCSS 的嵌套、变量、mixin 语法。同时 extends 里叠加了 stylelint-config-standard-scss 和 stylelint-config-clean-order这样 SCSS 文件既拥有专门的 SCSS 规则又保留了属性排序能力。对于 CSS Modules 文件文件名通常以 .module.css 结尾里面的类名是自动生成的哈希根本没法要求 selector-class-pattern所以单独关掉这个规则。对于普通 CSS 文件需要处理 Tailwind 的自定义 at-rule否则 tailwind base、apply text-center 这类的写法会被判断成未知语法。这里用 ignoreAtRules 把 Tailwind 常用的指令全部加白名单。这套三段式配置的好处是如果一个 SCSS 文件里想用 CSS Modules写成 “.module.scss” 命名那就同时命中了 SCSS 和 CSS Modules 两个 overrides 块规则会自动合并。实际使用时几乎没有需要手动写覆盖配置的情况。3.3 属性排序规则到底要不要开怎么开更合理属性排序一直是样式规范里争议最大的点。支持的一方认为排序能让代码结构清晰、减少 diff 冲突反对的一方觉得这是“强迫症”对实际运行毫无影响。我的态度比较明确在团队协作和 Monorepo 场景下属性排序值得开但要开得聪明。为什么 Monorepo 里更需要排序因为包多、改动频繁同一个文件被不同人改的概率更高如果没有固定排序合代码时经常出现同一个属性因为位置不同而产生多余冲突。而排序规则一旦固定配合 --fix 自动整理表面上看是代码整齐了底层其实是减少了不必要的 git diff让 review 的差异更聚焦在真实逻辑上。开排序的时候不建议自己从头写 order/properties-order 规则。理由是属性种类实在太多了你很难自己排出一套逻辑自洽的顺序。直接用 stylelint-config-clean-order 就行它提前定义好了展示属性、定位属性、盒模型属性、排版属性、视觉属性等几大类的内部顺序开箱即用。如果你有特殊要求比如想让某个业务属性强制放在最前面那可以用 overrides 在特定文件类型上单独追加不要去改全局配置。4. 把规范跑起来命令设计、增量检查和自动修复4.1 scripts 组织方式根命令跑全量包命令跑单个配置只是第一步真正让规范“跑起来”的是命令设计。在 Monorepo 里命令的组织方式通常有两个层次根目录的统一入口和子包的独立入口。根目录的 package.json 里我会放几条全局脚本比如{ scripts: { lint:style: stylelint \**/*.{css,scss,less}\ --ignore-pattern \**/node_modules/**\ --ignore-pattern \**/dist/**\, lint:style:fix: stylelint \**/*.{css,scss,less}\ --fix, lint:style:cache: stylelint \**/*.{css,scss,less}\ --cache --cache-location node_modules/.cache/.stylelintcache } }lint:style 是全量检查用于 CI 或者本地整体验证lint:style:fix 做自动修复lint:style:cache 是带缓存的增量检查一般作为本地开发时的主要命令。这里有个细节值得注意全量检查时一定要用 --ignore-pattern 排除 node_modules 和 dist这些目录里的文件本质上不是源码检查它们只会拖慢速度、制造噪音。子包层面我会在每个包的 package.json 里也加上 lint:style 脚本。因为子包和根目录的工作目录不同子包脚本简单写成调用根目录的命令即可。例如{ scripts: { lint:style: cd ../.. pnpm lint:style -- --filter ./packages/xxx } }这样既避免了重复安装依赖又保证了所有子包用的都是同一套工具版本和基础配置。根命令负责统一子包命令负责聚焦两层命令互相配合基本能满足所有日常场景。4.2 借助 lint-staged 做提交前检查Monorepo 下要注意路径问题规范要真正落地不能只靠 CI开发者在提交代码之前就应该发现问题。这里通用的方案是 husky lint-staged。在 Monorepo 里跑 lint-staged 有一个非常容易踩的坑lint-staged 默认是在 git 根目录执行的它会把匹配到的文件路径传递给你配置的命令。如果文件名是相对路径比如 packages/admin/src/style.css那么你在根目录执行 stylelint 时能正常解析但如果 lint-staged 配置在某个子包内部路径会变成相对的stylelint 可能就找不到文件了。我的做法是只在根目录配置 husky 和 lint-staged不在任何子包内重复配置。lint-staged 的配置文件长这样{ *.{css,scss,less}: stylelint --fix --cache --cache-location node_modules/.cache/.stylelintcache }这里用了 --fix因为 lint-staged 检查的是已暂存的文件如果有可自动修复的问题直接在暂存区修掉开发者只需要重新 git add 加入修复后的版本即可。我的实际经验是在 lint-staged 里加上自动修复能把 80% 的样式问题消灭在提交之前剩下 20% 的疑难杂症才需要开发者手工处理。还有一个坑如果团队使用 Windows 环境lint-staged 传输给 stylelint 的路径分隔符问题会导致匹配失败。遇到这种情况我建议统一团队使用 Git Bash 或 WSL 执行 git 操作否则 lint-staged 的路径解析可能因为系统差异而表现不一致。4.3 CI 里怎么提速缓存、增量扫描、并发一个都不能少CI 场景是整个 Monorepo 样式检查最容易翻车的地方。全量跑发生在每个 PR 上一个 PR 可能只改了一个文件却要把仓库里所有包的样式全检查一遍时间成本完全不划算。提速的核心思路是“只检查变更涉及的文件”。在 CI 里可以先用 git diff 获取变更文件列表再喂给 stylelintgit diff --name-only origin/main...HEAD -- *.css *.scss *.less | xargs stylelint --cache --cache-location node_modules/.cache/.stylelintcache这条命令会先拿到和主分支相比有改动的样式文件然后只对它们做检查。加上 --cache 之后第一次全量跑过的文件会生成缓存后续没有变动的文件会直接从缓存读取检查结果速度提升非常明显。我在实测中一个 40 个包左右的 Monorepo全量检查大约需要 40 秒改成增量 缓存之后单个 PR 平均检查时间降到 5 秒以内。CI 里还有一个参数值得加就是 --max-warnings。比如设置成 0意味着只要有任何 warning 级别的样问题CI 就直接失败。这个参数的作用是防止团队把 error 降级成 warning 之后“眼不见为净”最后 warning 积累了几百条规范形同虚设。如果确实有些规则暂时没法修复我建议先把它从配置中去掉而不是靠 warning 数量兜着这样规则表是干净的团队也不用天天刷屏。5. 实际开发中的高频报错与排查手册5.1 配置文件相关的报错CssSyntaxError 和 Unknown word用这套模板跑了几个月收集了一些高频报错整理成速查表方便团队遇到问题时直接查找。报错信息触发场景解决方案CssSyntaxError: Unknown word检查 SCSS 文件时没有配置 customSyntax在 overrides 里给 .scss 指定 postcss-scssUnexpected unknown at-ruleTailwind 的 apply / screen 等被误判在规则的 ignoreAtRules 中添加对应 at-ruleExpected indentation of 2 spaces版本升级到 16 之前的存量规则残留检查是否还有旧规则的覆盖或删除冗余配置Invalid option for rule “xxx”配置文件里写了已经不存在的规则升级 16 后检查自定义 rules 里是否引用了被移除的风格类规则Configuration for rule “order/properties-order” is invalid同时使用了 stylelint-config-clean-order 和自定义排序将 rules 里的 order/properties-order 置为 null这里想单独说下 CssSyntaxErrorUnknown word。我见过最典型的场景是某个人在 .scss 文件里用了 Vue 的 scoped 样式写了一些类似 ::v-deep 的伪类选择器但配置文件里没有覆盖到 .vue 文件stylelint 拿到的还是 .scss 的解析器。如果团队项目里同时存在 Vue 单文件组件和 SCSS需要在 overrides 里加上对 .vue 文件的 customSyntax 配置。Stylelint 16 中可以直接用 postcss-html 解析 Vue 文件中的样式块这个细节容易漏但漏了之后报错又极其让人困惑。5.2 版本升级带来的规则失效问题比想象中的更隐蔽从 Stylelint 15 升到 16 之后除了风格类规则被移除还有一个隐蔽的变化容易被忽略规则的默认值发生了变化。比如 15 里的 color-hex-case 默认要求小写十六进制颜色16 里这条规则直接没了15 里 indentation 还管着缩进16 里缩进完全交给 Prettier。如果你在升级后没有同步更新配置只是硬着头皮跑会看到各种奇怪的报错其实根源就是旧配置里的规则名已经不存在了。我的排查方法是升级后先跑一遍 --print-config 输出当前生效的完整配置再拿着它和官方文档逐条核对。如果某条规则名字在文档里搜不到直接删掉就行。这里要提醒一下很多人升级后习惯把旧的 .stylelintrc 里的风格类规则全部挪到 .prettierrc 里配置从“工程化”的角度看这个方向是对的但一定要确保 Prettier 用的规则和团队之前的风格约定一致否则格式化一次整个仓库的 diff 会爆炸。5.3 存量项目大规模接入的渐进策略不要一上来就 error之前提到的都是新模板的场景。但如果你的 Monorepo 里已经有一堆老项目样式代码积累了好几年直接上全套严格规则的结果一定是“满屏飘红”开发体验瞬间跌到谷底规范大概率会不了了之。针对存量项目我的建议是分三步走。第一步先用当前的配置做一次全量扫描把错误和警告数量统计出来但先不接入 CI。第二步在根配置里把暂时无法修复的规则降级为 warning然后设定 --max-warnings 为一个比较大的阈值比如 500保证 CI 先能过但会标记出问题数。第三步每次开发迭代顺手修复自己改过的文件当一个目录或一个包的 warning 数量归零后就把阈值调低同时把规则从 warning 恢复成 error。这三步走完通常需要一两个迭代周期但团队不会有任何“被规范突袭”的感觉。而且因为规则是逐步收紧的任何一次改动都只影响当前正在改的文件不会出现推倒重来的尴尬局面。这个方法在团队里实用性非常高推荐有历史包袱的项目试试。5.4 我最终保留的模板配置清单可以直接拿来用最后把整个模板的核心配置完整贴出来这基本就是我最终在团队模板里保留的版本。新版模板里同时兼容了 SCSS、CSS Modules 和 Tailwind 的常规场景也兼顾了新老项目的渐进式接入需求。{ extends: [stylelint-config-standard], plugins: [stylelint-config-clean-order], rules: { color-named: never, declaration-block-no-duplicate-properties: true, max-nesting-depth: 3, selector-class-pattern: ^[a-z][a-zA-Z0-9]*$, order/properties-order: null }, ignoreFiles: [ **/node_modules/**, **/dist/**, **/coverage/**, **/public/** ], overrides: [ { files: [**/*.scss], customSyntax: postcss-scss, extends: [stylelint-config-standard-scss, stylelint-config-clean-order] }, { files: [**/*.module.css], rules: { selector-class-pattern: null } }, { files: [**/*.css], rules: { at-rule-no-unknown: [ true, { ignoreAtRules: [tailwind, apply, variants, responsive, screen] } ] } } ] }完整配套依赖清单如下我用 pnpm 统一装在根目录pnpm add -D stylelint stylelint-config-standard stylelint-config-standard-scss stylelint-config-clean-order postcss-scss stylelint-config-tailwindcss这样一套配下来新建一个包时几乎不需要关心样式规范怎么配提交代码时 lint-staged 自动兜底CI 只查变更文件性能也不会拖后腿。我在实际使用过程中最大的体会是样式规范这件事难的不是规则本身而是让规则在不同技术栈、不同历史阶段的包之间保持“恰到好处”的力度。太松等于没有太紧又会被团队抵制。Monorepo 模板的意义就在于把这种力度的把握沉淀成配置、脚本和文档让任何新成员加入时都不需要重新踩一遍我们踩过的坑。
返回列表