上个月在给团队搭企业级 Monorepo 工程化模板的时候一位同事正好去面某大厂笔试里出现一道工程化题在 Monorepo 架构下如何保证多包之间的样式规范统一他当场愣住——ESLint 的配置背得滚瓜烂熟Stylelint 却完全没系统研究过。回来跟我吐槽说样式代码平时全靠自觉真要落到工程化就抓瞎了。我笑了笑没说话因为就在那周我刚把 Stylelint 从根目录一路配到十几个子包踩坑踩到怀疑人生最后沉淀出一套可复用的方法论。这篇就把完整过程整理出来从方案选型、配置拆解、CI 联动到各种报错排查不说废话全部基于可复现的实践。1. 为什么 Monorepo 模板里 Stylelint 是刚需1.1 从一次样式事故说起先讲一个真实事故。两个月前我们 C 端 React 子包里有位同事在公共样式文件里改了一个全局变量把某个基础色值从#1677ff调整成了#1668dc。本意是让统一品牌色更耐看但他改的是公共层级的 SCSS 变量而这个变量被 B 端中台子包直接引用。结果就是中台系统里所有按钮、链接、选中态的颜色一夜之间全变了用户在工单系统里截图投诉了三次最后运维把版本回滚才压住事态。复盘的时候大家发现问题根源根本不是“这个颜色该不该改”而是 Monorepo 模式下样式文件的引用关系太隐蔽了。你在子包 A 里写的变量、mixin、函数很可能被子包 B、C 以相对路径或 workspace 协议直接引用。代码编译能过、构建能过但视觉细节悄无声息地崩了。如果当时 Stylelint 已经上线并且配置了全局变量的只读规则、不允许子包直接引用公共层变量这种事故在提交阶段就会被拦截。很多人觉得 lint 是为了“看起来舒服”但把视角拉高一点Stylelint 在 Monorepo 里的核心价值是“建立样式代码的边界意识”。它能让一个几千人协作的大仓库重新变得可控谁改了公共样式、谁引入了非法颜色值、谁写了非标准语法机器会第一时间告诉你。1.2 Stylelint 在样式规范体系中的定位前端领域提“代码规范”时大家第一反应是 ESLint。JS/TS 的静态检查深入人心但样式这块长期是被忽视的。ESLint 管的是逻辑、变量、语法范式而 CSS/SCSS/Less 这些样式代码同样有语法错误、重复属性、无效颜色、未使用的变量、属性顺序混乱等问题这些靠 code review 根本看不全。Stylelint 就是样式世界的 ESLint。它的定位很明确对 CSS 及其预处理器语法做静态分析发现问题、自动修复、统一风格。放在 Monorepo 工程化模板里Stylelint 解决的问题可以拆成三层第一层语法与合法性检查。比如color: #fff;后面又写了一个color: #efefef这种重复属性block-no-empty空规则块property-no-unknown未知属性这些是底线问题。第二层可维护性约束。比如 hex 颜色是否可以用简写#ffffff写成#fff选择器嵌套深度是否超过三层!important是否被禁用类名单词是否用连字符而不是下划线。第三层团队风格统一。比如声明块的属性排序position永远在display前面display在flex相关属性前面这种视觉秩序在小项目里无所谓但在大 Monorepo 里几十个人同时改共享样式没有自动化的顺序管理和风格约束代码会迅速变成一锅粥。所以我的结论很直接Monorepo 工程化模板里ESLint 和 Stylelint 必须同时存在缺一个都不算完整的工程化。2. 从零初始化 Stylelint版本选型与基础落地2.1 工具链版本怎么选搭建 Monorepo 模板的第一步不是写配置而是选版本。这里我吃过亏先重点说版本问题。当前我写这篇时Stylelint 已经进入 v16 时代。v14 有一批 deprecated 的规则在 v15 正式移除v16 又干了一件大事移除了所有 stylistic 相关的规则官方理由是“样式风格问题应该交给格式化工具而不是 lint 工具”。这意味着过去很多人习惯的stylelint-config-prettier已经没有必要同时你不能再指望用 Stylelint 去检查“缩进应该是 2 空格还是 4 空格”“字符串用单引号还是双引号”这类纯美学的规则。这个决策我一开始挺反感觉得官方在“削功能”。但用得久了才理解stylistic 规则和 Prettier 天然冲突两个工具互相打架你改我、我改你最后只能靠stylelint-config-prettier关掉一堆规则来求和。v16 直接把这个问题从根上解决——Stylelint 专注语义和错误检查格式问题全权交给 Prettier。这种方式对 Monorepo 特别友好因为模板里同时存在 Prettier 配置两套工具的职责边界清晰新人接手时不用猜。版本选型上我建议直接用 v16不要抱着 v15 的旧项目配置搬家。v16 的 breaking changes 并不多最核心就是上面说的移除 stylistic 规则、需要 Node 18.12以及废弃的配置格式不再兼容。如果你用 Vite 构建Vite 5 已经适配 v16 没有问题。2.2 最小可用配置长什么样我先给出一份最基础的.stylelintrc.json这份配置可以直接放进 Monorepo 根目录所有子包共享{ extends: [ stylelint-config-standard ], ignoreFiles: [ **/dist/**, **/node_modules/**, **/coverage/** ], rules: { color-no-invalid-hex: true, block-no-empty: true, declaration-block-no-duplicate-properties: true, selector-class-pattern: ^[a-z][a-zA-Z0-9-]*$ } }安装依赖时需要区分清楚stylelint本身是运行时stylelint-config-standard是规则集。两个都要装到根目录 devDependenciespnpm add -D stylelint stylelint-config-standard这份最小配置能做什么color-no-invalid-hex拦截#ffGG00这种瞎写的颜色block-no-empty拦截空样式块declaration-block-no-duplicate-properties拦截同一声明块里重复的相同属性selector-class-pattern强制类名用小驼峰或连字符风格。这里要特别说明一个常见误区很多人以为“装了 Stylelint 就自动生效”其实 Stylelint 自带默认规则是关闭的你必须显式引入规则集或者手动配规则。extends字段加载stylelint-config-standard时它已经把一百多条规则按官方推荐值打开你只需要在此基础上覆盖少量个性化项。2.3 为什么选 pnpm workspace 管理依赖既然标题是 Monorepo 模板必然涉及包管理器的选择。我在模板里用的是 pnpm workspace这不是因为它“最火”而是依赖管理机制和 Stylelint 天然契合。pnpm 的 node_modules 结构和 npm/yarn 不一样它是硬链接符号链接的组合。对 Stylelint 这种插件生态丰富的工具来说最难受的问题就是“版本冲突”。npm 传统 node_modules 里主项目装了一个 Stylelint某个子包又装了自己的插件和自定义配置极容易因为 peerDependencies 版本不一致导致插件加载失败。pnpm 的严格依赖隔离虽然偶尔会让人抓狂但从另一方面逼迫你用 workspace:^ 协议统一版本反而让 Stylelint 及插件在多个子包之间共享同一套二进制和规则集避免“每个包都有自己的一份 Stylelint”这种诡异状态。模板根目录的 pnpm-workspace.yaml 长这样packages: - packages/*我建议所有stylelint相关依赖都只装在根目录子包不要单独安装。这样 CI 安装依赖时只需要跑一次Stylelint 的规则也被各包共享配置心智负担最小。3. Monorepo 多包场景下 Stylelint 配置策略3.1 共享配置与包级覆盖这是 Monorepo 和普通单包项目最大的区别。单包项目里一份.stylelintrc.json放在根部就完事了。Monorepo 里每个子包有各自的职责和样式选型比如packages/button用纯 CSSpackages/admin用 SCSSpackages/mobile用 styled-components。你不能用一套规则硬压所有场景也不能放弃统一放任自流。我的方案是根目录放一份基础共享配置包含所有包必须遵守的底线规则每个子包通过overrides字段做差异化覆盖。Stylelint 的overrides支持按 glob 匹配文件路径配合files和子包目录可以实现精确控制。比如根目录配置可以这样写{ extends: [stylelint-config-standard], overrides: [ { files: [packages/admin/**/*.scss], customSyntax: postcss-scss, rules: { max-nesting-depth: 3 } }, { files: [packages/mobile/**/*.{ts,tsx}], customSyntax: stylelint/postcss-css-in-js, rules: { color-hex-case: lower } } ] }overrides的匹配规则是从配置目录开始算的。如果配置放在根目录packages/admin/**/*.scss就能命中子包下所有 SCSS 文件。如果某个子包想要它的个性化规则在子包内放一个.stylelintrc.json也会被自动合并但有个优先级坑我在后面避坑章节专门说。对于子包级配置我推荐一个很实用的技巧在子包的package.json里加一条快捷脚本{ scripts: { stylelint: stylelint \src/**/*.{css,scss}\ --fix } }这样每个子包可以自主执行本地 Stylelint 修复但共享规则仍然是全局统一的那一套。3.2 与 VS Code 和 CI 的联动工程化模板的完整度要看开发体验和自动化链路是否打通。VS Code 侧需要装 Stylelint 官方插件然后在项目根目录放一份.vscode/settings.json{ stylelint.validate: [css, scss, less, vue, tsx], editor.codeActionsOnSave: { source.fixAll.stylelint: explicit } }这里explicit是新版编辑器对 codeActionsOnSave 的值写法老版本可能要求true具体看你项目锁定的 VS Code 版本。配置完之后保存文件时 Stylelint 会自动按规则集修复ESLint 走 ESLint 的修复两者互不干扰。CI 侧推荐用lint-staged配合实现“提交时只检查暂存的样式文件”。在根目录 package.json 里配置{ lint-staged: { *.{css,scss,less}: [ stylelint --fix, prettier --write ], *.{ts,tsx,js,jsx}: [ eslint --fix, prettier --write ] } }再加上 husky 的 pre-commit 钩子开发者在提交代码时Stylelint 只对本次变更的样式文件跑检查速度极快团队基本不需要关心“执行全局 lint”的过程。3.3 样式文件该不该放进 ESLint 的检查范围我见过不少团队的做法是用 ESLint 附带的eslint-plugin-css去检查 CSS。这个方向我不是很赞同。ESLint 是基于 JS 解析器的处理 CSS 需要走 custom parser 路线效率低规则覆盖也远不如专门做样式检查的 Stylelint 全面。比如 CSS 变量未定义、颜色格式错误、属性值不合法这些Stylelint 是原生支持ESLint 则需要靠插件模拟。在 Monorepo 模板里工具职责越单一越容易排查问题。JS 归 ESLint样式归 Stylelint格式归 Prettier三者边界清晰。你不需要在 ESLint 配置文件里写一堆“我该怎么处理 .css 文件”的 hack。4. 企业级样式规则集设计4.1 规则分级底线、推荐、个性化企业级模板不能把所有规则一把梭不然新项目接入时会被成百上千条报错淹没开发同学直接原地爆炸。我习惯把 Stylelint 规则分成三级底线规则不管什么项目不管什么风格都必须遵守。比如block-no-empty空块、color-no-invalid-hex无效颜色、property-no-unknown未知属性、string-no-newline字符串禁止换行。这些规则直接对应“代码写错”的场景必须开启没有讨论空间。推荐规则默认打开但允许子包按需覆盖。比如max-nesting-depth最大嵌套深度、selector-max-id限制使用 ID 选择器、declaration-block-no-duplicate-properties禁止重复属性。这类规则在大部分项目都能成立但总有人有特殊场景所以保留覆盖通道。个性化规则这是企业规范的核心差异点。包括 CSS 属性排序、类命名风格、颜色值书写格式、是否允许!important。这些规则没有绝对的对错完全取决于团队之前的代码习惯。我建议在模板里先开最小集等项目跑了一段时间、大家都有体感之后再通过团队讨论逐步增加。下面这张表可以直接抄作为规则集的起点规则作用级别block-no-empty禁止空样式块底线color-no-invalid-hex禁止非法十六进制颜色底线declaration-block-no-duplicate-properties禁止同一声明块重复属性底线max-nesting-depth限制 SCSS 嵌套深度推荐selector-max-id禁止 ID 选择器推荐color-hex-length十六进制颜色长度统一个性order/properties-order属性书写顺序个性unit-allowed-list限制允许使用的单位个性4.2 CSS 属性顺序与排序插件属性顺序是样式规范里最容易被忽略、却对阅读体验影响最大的点。一段 CSS 如果写两三百行position、display、flex、margin、padding、color、font-size穿插出现任何维护者都得来回滚动才能理清一个元素的空间布局关系。我使用的方案是stylelint-config-recess-order。它依照一个经典的排序逻辑先盒模型位置、尺寸、内边距、边框、外边距再排版字体、颜色、背景最后是其他视觉效果。安装pnpm add -D stylelint-config-recess-order在配置里 extended{ extends: [ stylelint-config-standard, stylelint-config-recess-order ] }启用之后Stylelint 会自动检查每个声明块里的属性顺序--fix可以自动排序。这块对 Monorepo 的价值在于多个团队同时维护共享样式时不再需要人工 review“你这个顺序不对”这种争吵机器统一了节奏。4.3 SCSS、Tailwind 和 CSS-in-JS 的共存策略Monorepo 模板里不可能只有纯 CSS常见场景是有的包用 SCSS 写组件库有的包用 Tailwind 做业务页面还有的包用 styled-components 或 emotion 在 TS 文件里写样式。Stylelint 对这三类场景的接入方式各不相同SCSS 场景用postcss-scss作为 customSyntax。装包pnpm add -D postcss-scss配置里加{ customSyntax: postcss-scss }加了之后Stylelint 就能解析mixin、include、$variable这些 SCSS 特有语法而不是把它们当非法 CSS 报错。Tailwind 场景Tailwind 的apply、tailwind指令和 PostCSS 的config这类自定义 at-rule直接跑标准 Stylelint 会报at-rule-no-unknown。官方推荐在 rules 里加白名单{ rules: { at-rule-no-unknown: [true, { ignoreAtRules: [tailwind, apply, config, screen] }] } }CSS-in-JS 场景比如styled-components需要在 TS/TSX 文件里检查模板字符串内的样式。这时要用stylelint/postcss-css-in-js作为 customSyntax同时需要单独写一个 Stylelint 入口因为配置文件默认不会去扫 TS 文件。我在模板里的做法是{ customSyntax: stylelint/postcss-css-in-js }然后在lint-staged里增加对.tsx文件的 stylelint 检查。要注意的是CSS-in-JS 环境下color-no-invalid-hex等规则仍然生效但selector-class-pattern这类依赖选择器上下文的规则往往要关掉因为模板字符串里的内容大部分是动态插值。5. 避坑指南我踩过的那些 Stylelint 坑5.1 插件版本冲突导致“配置不生效”第一个坑来自插件版本。装stylelint-config-recess-order时如果不注意它内置依赖的 Stylelint 版本极容易出现“配置加载成功但规则完全不生效”的诡异情况。我遇到的现象是运行stylelint命令没有报错但属性顺序完全不检查。排查到最后是stylelint-config-recess-order内部声明依赖stylelint^14而我的项目装的是 v16peerDependency 没满足导致插件加载时被静默忽略。解决办法很简单装完所有 Stylelint 插件后跑一次stylelint --version确认根目录实际加载的版本再执行pnpm why stylelint查看依赖树里是否有多个版本混用。如果出现了树形结构里两个不同大版本并存优先在根 package.json 里用pnpm.overrides强制锁定 Stylelint 版本{ pnpm: { overrides: { stylelint: ^16.0.0 } } }5.2overrides与子包配置的优先级陷阱Monorepo 第二坑是子包配置覆盖根配置时overrides的匹配顺序和子包配置的合并逻辑很容易搞混。我最初在packages/admin里放了子包级.stylelintrc.json希望它只覆盖自己目录下的 SCSS 规则。但实际执行时 Stylelint 会找“离文件最近”的配置文件如果子包自己有一份完整配置它不会自动合并根配置的extends而是整体替换。结果就是子包里的 Stylelint 变成“裸奔”一百多条标准规则全部失效。正确的做法是子包配置里显式继承根配置{ extends: [../../.stylelintrc.json], rules: { max-nesting-depth: 4 } }如果只是想在某些场景改几条规则优先用根配置的overrides而不是分发多份子包配置。5.3 误伤 CSS Modules 里的:global和变量命名CSS Modules 在 Monorepo 里很常见它的.module.scss文件里经常出现:global(.ant-btn)覆盖第三方组件样式的写法以及$--foobar这类 BEM 风格变量。默认的selector-class-pattern和custom-property-pattern会把这些全部报错。我处理这类问题的方式是在overrides里单独为*.module.scss设置宽松规则{ files: [**/*.module.scss], rules: { selector-class-pattern: null, custom-property-pattern: null } }这个配置的收益很大正常源代码用严格规范第三方样式覆盖场景放行不会因为 lint 报错强迫开发者写一堆stylelint-disable注释。5.4 性能问题大仓库扫描慢Monorepo 规模上来后Stylelint 全量扫描几百个 SCSS 文件可能要跑十几秒这在 CI 里很难接受。我的优化手段有三板斧第一用ignoreFiles把dist、node_modules、coverage、PNG/SVG 等非目标文件排除在外。第二在lint-staged里只检查暂存文件避免全量扫描。这招效果立竿见影提交时基本感觉不到 lint 的存在。第三给 CI 加缓存。GitHub Actions 或 GitLab CI 里依赖安装阶段用缓存目录挂载node_modules/.cache/stylelintStylelint 自带了基于文件元数据的缓存机制。命令里加--cache --cache-location node_modules/.cache/stylelint/.stylelintcache即可stylelint packages/**/*.{css,scss} --cache --cache-location node_modules/.cache/stylelint/.stylelintcache5.5 vue 文件里 style 块检查Vue 单文件组件里style块需要额外配置。如果只是把stylelint跑在.vue文件上默认是解析不了的。我的配置是在overrides里加{ files: [**/*.vue], customSyntax: postcss-html, rules: { no-empty-source: null } }postcss-html可以让 Stylelint 正确识别script、template、style的分隔边界。同理.astro文件也可以通过自定义postcss-html语法支持。6. 常见问题速查表与调试技巧这一节我把实际操作中遇到频率最高的问题整理成速查表后面谁遇到可以直接照着查。现象可能原因排查/解决运行 Stylelint 没有任何输出配置里没有打开任何规则检查 extends 是否引入规则集SCSS mixin 被报语法错误没有配置 postcss-scss安装 postcss-scss 并设置 customSyntaxTailwind apply 被报 at-rule-no-unknown默认规则不认识自定义 at-ruleignoreAtRules 加 tailwind/apply.vue文件的 style 块不检查缺 postcss-html syntaxoverrides 配置 customSyntax--fix 之后代码格式被改乱和 Prettier 冲突v16 已移除 stylistic 规则用 Prettier 管格式子包配置不生效/规则失效子包配置覆盖了根 extends子包 extends 显式继承根配置CI 里偶现出不来 lint 错误缓存了旧文件状态加 stylelint --cache 后注意清缓存VS Code 保存时不自动修复settings.json 权限或插件未识别确认 stylelint.enabletrue 和 validate 配置排查工具方面我强烈建议用stylelint --debug看配置加载过程它会输出每个 glob 命中的文件列表和 config 对象内容。这在 Monorepo 多级配置下是救命稻草能够快速定位“哪个文件被哪份配置接管了”。还有一个效率技巧把 Stylelint 命令做成根目录的统一脚本子包不需要重复写scripts: { lint:style: stylelint \packages/**/*.{css,scss,less}\ --cache --fix }这样 CICD 和本地执行走同一条命令我又在脚本里加了--formatter table报错信息对齐成表格一眼就能看到文件名、行号、列号和建议人都不用开编辑器就能判断问题。7. 把 Stylelint 模板化之后团队获得了什么这块算是我个人的实践收尾不展开长篇大论。搭建这套 Monorepo Stylelint 模板前前后后花了两周其中至少一半时间在踩版本和插件兼容性的坑。但沉淀成模板之后收益立刻显现。新接入的子包开发者安装完依赖.vscode/settings.json和.stylelintrc.json直接用根目录那份保存文件自动修复提交代码自动检查从第一天起写出来的样式就是符合规范的。我个人最深的一点体会是样式规范这件事靠文档约定是维持不住的必须靠工具强制。文档写一百遍“属性要排序”不如在 CI 里跑一次 Stylelint 直接让不排序的代码无法合入。尤其是 Monorepo 这种多团队协作场景公共样式共享面积大一次低级错误的影响面可能被放大到所有子包。有一层自动化的底层防线比任何评审流程都可靠。如果你正在搭自己的 Monorepo 模板我建议不要一开始追求大而全的规则集先配置好stylelint-config-standard这一套底线再根据自己的技术栈把 SCSS、Tailwind、Vue 的 customSyntax 配好跑通 VS Code 保存修复和 CI 拦截链路。之后有精力了再逐步加入属性排序、颜色格式、命名约束这类个性化规则。工具先入场规则慢慢磨合这样团队阻力最小规范也最容易落地。最后再分享一个我常用的“后手”给样式规范迭代留一个README-STYLELINT.md每次团队讨论新增或调整规则时把理由和示例写进去而不是只改配置。编码规范最怕“规则在但没人记得为什么”。有历史记录后来的人维护规则时才不会把它改成另一套风格。
