ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Monorepo 工程化下 Stylelint 样式规范配置实践与踩坑指南

Monorepo 工程化下 Stylelint 样式规范配置实践与踩坑指南 1. 项目概述1.1 核心需求解析如果你正在负责团队工程化基建大概率会面临这样一个场景代码仓库要从零改成 Monorepo 架构pnpm workspace 已经跑通了ESLint 也配得七七八八轮到样式这块的时候发现网上资料突然就稀薄了。随便搜搜要么是单包项目的 Stylelint 配置教程要么是“照着官网装一下就行”的老生常谈。等真在 Monorepo 里跑起来各种问题接踵而至——配置继承路径不对、scss 解析报错、lint-staged 只检查了一个包的样式、CI 上莫名其妙多出几千条 warning——这时候你才会意识到样式规范在 Monorepo 里的复杂度一点都不比 JS 规范低。这篇博文要聊的就是我从零搭建一套可复用的企业级 Monorepo 工程化模板时在 Stylelint 这部分踩过的坑、做出的取舍以及最终沉淀下来的一套能直接抄作业的方案。内容会覆盖三个层面先说清楚在 Monorepo 场景下 Stylelint 的定位和设计思路再给出一份完整的、经过验证的配置实现最后整理我实际碰到过的高频问题和解法。1.2 适合谁来参考这套内容主要面向两类人一类是正在从零搭建团队前端基建、需要把代码规范做成公司级或部门级模板的开发者另一类是已经把项目切到 Monorepo、但样式检查还停留在“装了但没完全装”状态的工程师。无论你用的是 pnpm、yarn 还是 npm workspace核心思路都通用差异点我会在文中单独标注。2. 整体设计与思路拆解2.1 Monorepo 里样式规范为什么这么容易翻车很多人在单包项目里用 Stylelint 用得挺顺换到 Monorepo 就开始出事根源在于一个认知错位Monorepo 的 lint 体系不是“一个项目一套配置”而是“多个子项目共享一套基线同时保留局部定制能力”。单包项目里你只要在根目录放一个.stylelintrc.json依赖统一装在 devDependencies 里就完事了。到了 Monorepo光“配置文件放哪”就够你纠结一阵。放根目录所有 workspace 包都能继承但个别包想用不同的规则比如某个纯组件库不用考虑页面级样式限制就得做覆盖覆盖层级一多排查问题的成本直线上升。放各子包里灵活性倒是有了但同一个规则改一个值要挨个改动十几个包的配置文件维护成本爆炸。我最终选的是“根级基线 包级覆盖”的混合策略。根目录维护一份严格的、面向全仓库的基础配置作为底线子包通过extends继承根配置再按自身需求追加或者覆盖少量规则。这样既保证了全局一致性又给特殊场景留了口子实际上也是大多数成熟开源 Monorepo比如 Babel、Vue Core的通用做法。2.2 定位Stylelint 在规范体系里的边界搭建规范体系时最容易犯的错是把 Stylelint 当成“万能样式工具”什么规则都往里塞。我见过有些团队的 Stylelint 配置写了六七百行连“颜色值必须小写”这种琐碎规则都管结果就是 CI 上天天红开发体验极差最后整个 stylelint 流程被当成形式主义废弃掉。正确的思路是Stylelint 只负责“静态语义检查”也就是那些不跑浏览器就能确定对错、且会影响代码质量和可维护性的点。格式化相关的问题缩进、换行、引号风格应该交给 Prettier不要通过 Stylelint 规则去管。两者一旦职责重叠就会出现“Prettier 改完 Stylelint 还报错Stylelint 改完 Prettier 又给你改回去”的死循环。我在模板里定的分界线是这样的检查维度归属工具典型规则可维护性Stylelintdeclaration-block-no-duplicate-properties、block-no-empty可访问性Stylelintcolor-no-invalid-hex、selector-pseudo-class-no-unknown错误预防Stylelintproperty-no-unknown、string-no-newline代码风格Prettier缩进、引号、逗号、换行逻辑约束ESLint配合插件CSS-in-JS 里的模板字符串、emoji 使用对应到分工上Stylelint 的检查结果分成 error 和 warning 两个级别error 直接让 CI 失败warning 只提示不阻断。这个设置在大型 Monorepo 里特别重要因为不同子包对样式规范的接受程度不一样全部一刀切 error 会导致模板在推广阶段就遭到强烈反弹。2.3 选型对比Stylelint 家族到底怎么挑Stylelint 本身是核心引擎真正干活的是它周围的一圈配置包和插件。我见过不少新手在这块栽跟头装了一堆包不知道各自是干嘛的或者装重复了导致配置互相打架。以我当前的模板为例依赖清单如下pnpm add -D stylelint stylelint-config-standard stylelint-config-recommended-scss stylelint-config-prettier stylelint-order postcss postcss-scss逐个说下每个包的必要性。stylelint本体不用解释。stylelint-config-standard是官方推荐的基线配置包含了 200 多条规则上面说的“错误预防”和“可维护性”检查基本都靠它。stylelint-config-recommended-scss是为了支持 scss 语法它依赖postcss-scss来做语法解析。stylelint-config-prettier用来关掉那些跟 Prettier 冲突的 Stylelint 规则避免两个工具打架。stylelint-order提供 CSS 属性排序能力这个是团队规范里比较有存在感的一个插件争议也比较大下文会专门聊。有两点需要提醒。第一stylelint-config-prettier的逻辑不是“让 Stylelint 用 Prettier 的规则”而是“把 Stylelint 里那些管格式的规则全部关闭”所以它应该放在extends数组的最后一位。第二如果你用的是 styled-components 或者 emotion 这类的 CSS-in-JS 方案需要额外装stylelint-config-styled-components并配置对应的处理器这个在实际项目里也很常用我后面会展开讲。3. 核心配置实现与实操要点3.1 根级配置一份托底的基线规范在 Monorepo 根目录新建.stylelintrc.json内容如下{ extends: [ stylelint-config-standard, stylelint-config-recommended-scss, stylelint-config-prettier ], plugins: [stylelint-order], rules: { order/properties-alphabetical-order: null, order/properties-order: [ { properties: [ position, top, right, bottom, left, z-index, display, flex, flex-direction, flex-wrap, justify-content, align-items, align-content, width, height, min-width, max-width, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, border, background, color, font, font-size, line-height, opacity, visibility, transition, animation ], unspecified: bottomAlphabetical } ], color-hex-length: long, color-named: never, selector-max-id: 0, selector-no-qualifying-type: true, max-nesting-depth: 3, scss/at-rule-no-unknown: [ true, { ignoreAtRules: [tailwind, apply, variants, responsive, screen] } ] } }这份配置里做了几个关键决策我逐个说下理由。color-hex-length设成long而不是默认的short是因为在 Monorepo 多包协作时长十六进制颜色#ffffff的可读性比缩写#fff更好尤其是当设计师直接抄设计稿上的色值给前端时#ffffff比#fff更容易对应到设计稿。color-named: never禁止使用red、blue这类命名颜色强制使用十六进制或rgb/hsl这是为了颜色统一管理方便后续做主题换肤时全局搜索替换。selector-max-id设为 0 就是彻底禁止#app、#header这样的 ID 选择器。这条规则在组件化开发时代争议不大ID 选择器优先级太高容易造成样式覆盖困难尤其是在 Monorepo 里多个团队共用一个组件库时样式优先级冲突是最难排查的问题之一。max-nesting-depth设为 3这是基于 scss 的实际场景设的一个经验值。完全禁止嵌套不现实但嵌套过深会导致编译后的 CSS 选择器层级变成.header .nav .list .item .link看着就头晕。3 层以内既足够表达常见的 BEM 结构又不会让代码太难维护。3.2 scss 场景下的特殊处理stylelint-config-recommended-scss并不是一个独立的规则集它更像是一个接线层把postcss-scss的解析能力和 scss 特有的规则接入 Stylelint。它解决了几个实际问题让 Stylelint 认识 scss 的变量$var、混入mixin、占位符%placeholder等语法否则这些符号会被当成未知代码报错。但 scss 和 postcss 的兼容性是容易踩雷的地方。Stylelint 16 版本开始默认使用 postcss 8 的 API如果你项目里还残留着 postcss 7 时代的插件大概率会出现“插件加载成功但规则不生效”的诡异问题。解决方案是在安装时锁定大版本pnpm add -D postcss8 postcss-scss4 stylelint16 stylelint-config-standard36版本锁定的意义在于当模板要推广到多个团队时依赖版本漂移是最大的不确定性来源。我见过不止一次两个团队用的模板配置一模一样但因为pnpm-lock.yaml不一致一个正常一个报错。所以我在模板里专门加了一条.npmrc配置来固定 pnpm 版本配合 lockfile 保证整个 Monorepo 的依赖树完全可复现。3.3 Prettier 集成避免规则打架的唯一正确姿势Stylelint 和 Prettier 的集成说难不难说简单也容易出错。核心就一件事让 Stylelint 放弃所有跟排版相关的规则完全交给 Prettier。stylelint-config-prettier干的就是这件事。它会把所有可能跟 Prettier 冲突的规则关掉比如缩进大小、引号风格、冒号空格、逗号空格等。这里有个容易犯的错误是有人会把stylelint-config-prettier放在extends的中间位置后面的配置包又把这些规则重新开启了。正确的顺序一定是把 prettier 配置放在extends数组最末尾保证它兜底。除了配置顺序还有一个容易出问题的点是stylelint-order和 Prettier 的关系。stylelint-order管的是“属性出现的先后顺序”Prettier 不管这个所以两者不会冲突。但是stylelint-order本身有一个order/properties-alphabetical-order规则按字母排序属性这个规则和团队约定的“物理属性分组排序”先定位再盒模型后视觉是有冲突的。我选择把字母排序关掉用properties-order自定义分组配合unspecified: bottomAlphabetical意思是在我列出的分组之外其余属性按字母序排在后面。这样既能保证高频属性position、display、margin 等的顺序一致又不需要强制开发者记住所有属性的分组归属。4. 实操流程与 Monorepo 集成交付4.1 从零到可用的五步落地配置文件的编写只是第一步真正让 Stylelint 在 Monorepo 里跑起来并起到规范作用还需要完成脚本、编辑器、pre-commit、CI 四个环节的串联。我整理了一份标准的落地顺序。第一步安装依赖并创建根级配置也就是上面那一整段 JSON 文件。完成后先在任意一个子包跑一次pnpm exec stylelint packages/**/*.{css,scss}验证是否能正常解析。这一步会暴露绝大多数环境问题版本冲突、语法解析失败等建议单独调整到通过为止。第二步给各子包创建继承配置。大多数子包不需要额外配置但为了让命令能稳定执行每个子包的根目录要有一个最小的.stylelintrc.json{ extends: [../../.stylelintrc.json] }这里注意extends里的路径一定要是相对于子包的相对路径。如果你写的是绝对路径或者依赖包名在 pnpm 的严格依赖隔离机制下子包很可能解析不到根配置。第三步在根目录package.json添加统一的 script。我的模板里用的是{ scripts: { lint:style: stylelint \packages/**/*.{css,scss,vue}\ --cache --max-warnings 0, lint:style:fix: pnpm lint:style --fix } }--cache参数会把已检查通过的文件缓存下来第二次执行时只检查变更文件。这个参数在 Monorepo 里价值极高因为packages/**这个通配符会扫到所有子包文件数量动辄几千个没有缓存的话每次全量检查都要十几秒开发者很快就懒得跑了。--max-warnings 0表示只要有一个 warning 就让命令退出非零状态。这一步是我在推行规范时被骂得最惨的一刀但从结果看非常值。因为在没有这个参数的时候很多人根本不看 warning 提示只在 CI 失败时才被迫处理 errorwarning 越积越多最后变成几千条无人问津的“历史遗留问题”。第四步配置 pre-commit 只检查暂存区的样式文件。用 lint-staged 按文件后缀分流{ lint-staged: { *.{js,ts,vue}: [eslint --fix, prettier --write], *.{css,scss,vue}: [stylelint --fix, prettier --write] } }注意vue文件同时出现在两个规则里这看起来像重复实际是正常的。Vue 单文件组件里既有 script 块又有 style 块ESLint 和 Stylelint 都要处理。lint-staged 会依次执行两个命令ESLint 只会 lintscript部分Stylelint 只会 lintstyle部分两者互补。第五步把样式检查接入 CI。这步最容易被忽略因为很多团队觉得 pre-commit 已经挡住了问题。但实际的工程经验是不所有人都装了 lint-staged 的 IDE 插件也有人会用--no-verify跳过 hookCI 是最后一道防线。在 CI 的 lint 阶段加上pnpm lint:style确保合入主分支之前的代码一定经过了完整检查。4.2 子包定制既要统一又要灵活纯前端的业务组件库和纯 Node 端的工具包样式规范的需求是完全不同的。Monorepo 的优势就在于可以针对不同场景做差异化配置而不需要拆仓库。我在模板里习惯把 workspace 包分成三类各自的样式配置策略如下包类型样式规范策略典型包名业务应用web继承根配置不覆盖apps/web、apps/admin共享组件库继承根配置追加组件特有检查packages/uiNode 工具包不启用 Stylelintpackages/utils、packages/configNode 工具包不走 Stylelint 的原因是这里根本没有 CSS 文件硬要加只会增加无意义的检查和报错噪声。有一种更轻量的做法是在根配置里用ignoreFiles把纯后端包排除掉。我把ignoreFiles统一放在根级配置里这样即便某个子包漏建了自己的配置文件也不会因为这个包里的.css文件去跑错误的检查。组件库追加的检查比较典型的是 BEM 命名规范。如果团队想强制组件类名遵循 BEM可以在packages/ui子目录下建一个独立配置{ extends: [../../.stylelintrc.json], rules: { selector-class-pattern: ^(block|block__element|block--modifier|block__element--modifier)$ } }这套组合拳打下来既保证了基线统一又让有特殊需求的包可以随时加装自己的规则而不会影响其他包。4.3 双端文件类型支持css-modules、less、Vue SFC实际企业项目里样式的书写方式很少是单一的 css/scss。如果你的 Monorepo 模板要具备通用性建议从一开始就支持常见的双端环境和框架差异。Vue SFC单文件组件的支持比较简单Stylelint 内置了对style块的处理能力加上postcss-html之后就能读取.vue文件pnpm add -D postcss-html然后在根级配置里加一个overrides块{ overrides: [ { files: [**/*.vue], customSyntax: postcss-html } ] }这里其实有一个隐藏很深的坑我敢说大多数人第一次遇到都会卡一下。postcss-html既然要解析 Vue 文件那么它对应的stylelint-config-standard版本不能和postcss-html有语法解析冲突。如果你用的 Stylelint 是 15 以下的老版本需要额外配置customSyntax: postcss-html这个写法在新版本也能用。如果用 Stylelint 16插件体系从 “stylelint-processor-html” 迁移到了 “postcss-html”别把老文章里的推荐直接抄过来我在后面“常见问题”部分会专门讲这个版本差异。如果你用的是 Tailwind CSS需要在scss/at-rule-no-unknown的ignoreAtRules里把tailwind、layer、apply等指令加进去否则tailwind base;会被当未知 at-rule 报错。上面的根配置我特意加了这个实际项目里这一步几乎是必做的。4.4 CSS Modules 的类名检查策略另一个高频场景是 CSS Modules。当项目里用了*.module.css这类文件时selector-class-pattern这类命名规则很容易误伤——CSS Modules 在编译后会自动生成xxx__abc__123格式的类名但源码里你写的可能是styles.container。正确做法是让 Stylelint 对 CSS Modules 文件放宽类名规则或者干脆跳过这类文件的类名校验。我更推荐的方案是给 CSS Modules 单独开一条路径{ overrides: [ { files: [**/*.module.{css,scss}], rules: { selector-class-pattern: ^[a-z][a-zA-Z0-9_-]*$ } } ] }这里用了一个宽松的驼峰命名模式。因为 CSS Modules 的文件在 JS 里访问时是以对象属性的方式出现的类名转成驼峰是常见操作太严格的类名校验比如强制小写连字符反而会让开发和规范打架。5. 常见问题与排查技巧实录5.1 高频问题速查表以下是我在实际搭建和维护这套模板过程中遇到频率最高的几类问题以及对应的解法问题现象根本原因解决方案Unknown word解析错误用默认的 postcss 解析器读 scss 语法安装postcss-scss在配置里指定customSyntax配置继承后规则不起作用子包 extends 了错误路径或者依赖包没有安装到根 devDependencies确认 extends 用相对路径依赖统一装在根目录Cannot find module stylelint-config-standardpnpm 严格依赖隔离子包无法访问根目录未声明的依赖把所有 stylelint 相关依赖装到根目录 devDependencies或使用pnpm.overrides强制统一版本属性排序规则在 Prettier 运行后被破坏stylelint-order 和 Prettier 的格式化冲突确认 stylelint-config-prettier 在 extends 最后一位属性顺序交由 Stylelint 的 fix 处理不要依赖 Prettier 排序CI 上 lint 通过但 local 报错本地和 CI 的 stylelint 版本不一致锁版本 锁 lockfile用 pnpm 的packageManager字段固定包管理器版本.vue文件样式没有被检查没有配置postcss-html的customSyntax按上文 overrides 配置添加 postcss-htmlscss 变量名被警告no-duplicate-selectors变量定义的$xxx被当选择器处理检查是否真的选了stylelint-config-recommended-scss并正确配置 postcss-scss5.2 排查实战一次 cache 引发的心跳暂停这里分享一次让我印象深刻的排查经历。模板推广到某个业务团队后反馈说代码里明明改了一个 scss 变量名但 Stylelint 在 CI 上死活不报错本地跑又能报。我远程连上去看了半天发现问题出在缓存文件名上。我在根配置的 script 里用了--cache参数Stylelint 会在当前工作目录生成一个.stylelintcache文件。在第一个人本地跑过之后这个文件被提交到了仓库我没有在.gitignore里排除它CI 拉代码的时候拿到了这个缓存文件于是它认为这个文件没变化直接跳过了检查。这是我在模板推广过程中踩过的最尴尬的一个坑解决方式有两个第一在.gitignore里加上.stylelintcache和node_modules/.cache保证缓存文件永远不进入版本控制。第二在使用 CI 时在 lint 命令前加一步pnpm exec stylelint --cache-clean或者干脆保证 CI 环境相对干净不携带本地缓存。这个坑很隐蔽的原因在于它不会报任何错误只是“静默地跳过”了你想要检查的文件把你的 CI 安全网开了一个大口子。5.3 版本兼容矩阵这些版本组合我实测稳定Stylelint 的版本迭代速度不快但每次大版本升级都会有 breaking change。我把自己实测过稳定的组合整理成了一张表供参考Stylelint 版本对应标准配置postcss-scss备注14.xstylelint-config-standard24.x4.x老项目可用需要额外装 postcss815.xstylelint-config-standard34.x4.x引入了废弃规则迁移成本中等16.x推荐stylelint-config-standard36.x4.x最新版修复了插件兼容性推荐新项目直接上特别说明一点Stylelint 16 开始stylelint-processor-html这个包已经停止维护处理.vue和.html文件统一改用postcss-html。网上很多文章还在推荐去装stylelint-processor-html如果你照着配会遇到Unknown processor或Cannot find module之类的错误可以直接忽略这类过时建议按我这个版本来。另外如果你在模板里用了stylelint-scss的规则比如scss/dollar-variable-pattern、scss/at-rule-no-unknown等注意stylelint-config-recommended-scss自带了一部分stylelint-scss的规则依赖不需要单独装stylelint-scss。但某些项目需要自定义变量命名规则时还是得把stylelint-scss作为插件显式注册。这个逻辑和 ESLint 里eslint-plugin-vue的关系类似不要搞混了。6. 最后再分享一点个人经验如果你也是第一次把 Stylelint 搬进 Monorepo我个人的建议是不要一上来就追求“全量规则都开满”宁可先少管跑通了再逐步收紧。拿我自己的落地过程来说第一版模板只开了错误预防类和代码可维护类的规则大概 80 条左右CI 全绿之后才在第二版加入属性排序和 BEM 命名审查让团队有个缓冲期去适应。这个渐进式推行方式比我之前“一步到位”的激进做法效果好得多至少大家不会一边改代码一边骂 spec。还有一个值得多说一嘴的点样式规范不像 JS 规范那么受重视但它对团队协作效率的影响是实打实的。当两个团队在 Monorepo 里共享一个组件库时样式冲突和优先级问题是最难协作的地方之一。一个强制统一的 Stylelint 基线能让这类问题在代码合入之前就暴露出来而不是等到 UI 走查时才发现灰度环境的样式乱了再回头人肉排查是哪条规则导致的覆盖。搭建企业级 Monorepo 工程化模板Stylelint 这部分看起来是最不起眼的环节但把它做扎实了后续的维护成本能省下一大截。希望这篇内容能帮你少走点弯路。
返回列表