用Sass Partial拆分CSS:从4800行到模块化的样式管理实践
前阵子接手一个维护了两年多的后台项目翻了翻样式目录一个 main.css 有 4800 多行改一个按钮颜色得全局搜索改完了还得提心吊胆怕影响别的地方。这种一坨CSS的日子相信不少前端朋友都经历过。后面我用 Sass 的 Partial 功能把这套样式重新梳理了一遍拆成十几个职责清晰的小文件维护成本肉眼可见地降下来了。这篇文章就围绕 CSS 配合 Sass 的 Partial 功能展开讲怎么用分文件的方式管理项目样式代码适合正在做中大型项目的开发者、刚接触 Sass 的新手以及被历史遗留样式折磨得想重构的维护者。读完你会对 Partial 的运行机制、目录划分逻辑以及实际落地时会踩的各种坑心里有数。1. 从一坨CSS到处处补丁为什么要引入Partial1.1 我接手那个4800行CSS文件时的真实困境这个项目其实不算特别大十几个页面但样式文件已经堆到4800多行。最大的问题不是行数多而是你永远不知道某一个类名到底在哪里定义、影响了哪些页面。经常遇到的情况是你在全局搜.btn能搜出七八十个结果其中大约三分之一是历史遗留的死代码三分之一是只在特定页面用了一次的局部覆盖真正属于公共组件的大概只有十几个。这种时候绝大多数开发者的操作方式就是打补丁。不改原样式新写一个.btn-new加一个更高的z-index或者在页面里塞一段style标签。补丁越打越多样式表越来越乱最后变成谁都不敢动的危房。其实问题本质不是 CSS 不好用而是没有一套合适的组织方式。如果只是简单地把一个 CSS 文件拆成十几个文件用link标签在 HTML 里逐个引入又会带来新的问题一是浏览器要多发十几个请求二是文件之间变量无法共享三是开发者必须自己保证引入顺序一个不小心就被后面的样式覆盖了。这也是很多人拆了又后悔的原因。1.2 Partial到底解决了什么问题Sass 里的 Partial翻译过来就是局部文件语法上就是一个用下划线开头的.scss文件比如_variables.scss、_buttons.scss。它和普通.scss文件最大的区别是编译器知道这个文件只是零件不会把它单独编译成一个独立的.css文件而是等其他文件用use或import引入后再把它合并进最终的样式表。这个机制带来的直接好处有三个。第一个好处是拆了但不增加请求。你尽管把样式拆成几十个 Partial最终编译出来的可能还是那一个main.css浏览器只需要加载一次。第二个好处是变量和混入可以在文件之间共享。传统 CSS 做不到这一点而通过use引入 Partial所有文件在编译时处于同一个上下文中自然就解决了。第三个好处是依赖关系变得显式。哪个文件用了哪些变量、哪些混入在文件头部就能看明白而不需要在全局脑补。1.3 它和普通多CSS文件方案有什么区别我用一个表格来对比三者的差别这样更直观。对比维度单个大CSS文件多个CSS文件link引入Sass Partial维护难度高改一处牵连全站中但顺序容易乱低职责清晰浏览器请求数1个N个1个编译后变量/嵌套共享不支持不支持支持构建能力无无有合并、压缩、源映射组件化弱弱强说实话早期我一度觉得多CSS文件link也算模块化了直到项目里出现某个公共样式的覆盖时序问题排查了大半天才明白是引入了第二个 CSS 文件导致的。从那以后遇到需要拆样式的项目我基本都是直接上 Sass 的 Partial 方案。它本质上就是把文件拆分和产物合并分开处理源码端可以拆得很细产物端依然只有一个文件两边的好处都拿到了。2. 下划线命名法与编译规则读懂Partial的运行机制2.1 一个下划线决定了文件命运Partial 最关键的名字规则是文件名以英文下划线_开头。注意这个符号是键盘上减号旁边的下划线_不是连接符-。比如你新建_variables.scssSass 就知道这是一个局部文件如果叫variables.scssSass 就会在编译产物的目录里生成一个同名的variables.css。很多人第一次用的时候会在这上面栽跟头。引用 Partial 时use后面写的是去掉下划线、去掉扩展名的路径。举例来说如果文件位于styles/abstracts/_variables.scss那么在main.scss里这样写use abstracts/variables;编译时 Sass 会自己去找到_variables.scss。注意这里的路径是相对当前文件所在目录的不需要写_也不需要写.scss。可以验证一下执行一次编译去输出目录看如果出现了_variables对应的 css 文件或者一堆多出来的 css 文件说明你把下划线写漏了。这个检查方法很简单但能省下大量排查时间。2.2 use、import、forward现在到底怎么选Sass 官方从一开始建议使用use替代import这三者的实际区别很大。import老写法把文件内容原样复制到当前文件里会重复加载变量污染全局作用域属于历史包袱。use现代默认推荐。每个文件最多被加载一次引入的内容默认挂在命名空间下比如use variables之后要用variables.$primary-color来取变量好处是避免重名冲突。forward不直接把内容引入当前文件而是转发给后续的使用方适合做索引文件或组件库的出口。我整理了一个表格方便对比指令加载方式作用域重复加载问题推荐场景import文本复制全局会老项目迁移过渡use编译加载命名空间每个文件一次现代项目默认forward转发导出命名空间不直接使用库文件/索引文件出口我现在项目里统一用use如果某个目录下的 Partial 需要被大量引用就用一个_index.scss把它们forward出去这样外部只需要写一行use components就能引入整个组件包清爽很多。forward还有一个用途就是可以把变量重新命名后再转发避免对外暴露太笼统的名字。2.3 编译过程与产物形态从源码到产物Sass 的处理链路大概是这样的读取入口文件比如main.scss。解析其中的use/import/forward指令构建依赖图。按依赖关系把各个 Partial 的内容合并进编译上下文。最终一次性输出 CSS 文件。因为 Partial 不单独进入产物所以你在输出目录里看到的只有一个main.css除非某个文件没有以下划线开头才会多出独立文件。这种机制有点像模块打包源码分很多个零件产物只有一两个文件。等到将来需要做按需加载或 Critical CSS 的时候你甚至可以单独编译某个非下划线文件生成只包含该模块样式的独立 CSS这是普通多文件方案很难优雅实现的。编译命令方面最常用的就两个sass styles/main.scss dist/main.css --styleexpanded sass --watch styles/:dist/ --stylecompressed第一个是单次编译第二个是监听整个styles目录的变化。--style参数可以控制输出格式expanded适合开发时调试compressed适合上线。如果有需要还可以加上--source-map生成 source map这样在浏览器调试时可以直接定位到对应的 Partial 文件而不是压缩后的一整行。3. 实战案例搭一套基于Partial的样式项目骨架3.1 目录结构按职责分层而不是按页面切分我见过不少初学者是这样拆的一个页面一个文件home.scss、about.scss、contact.scss。想法没错但页面之间往往有大量共享的组件样式按页面拆会导致两个问题要么同一个组件的样式在多个文件里重复定义要么改一个组件要改动 N 个页面文件。后来我改成了按职责分层的方式这也是目前社区里比较常见的一种组织思路。以一个中型内容网站为例目录长这样styles/ ├── main.scss # 入口文件只做聚合 ├── abstracts/ # 抽象层不产生实际CSS │ ├── _variables.scss # 全局变量颜色/字体/间距/断点 │ └── _mixins.scss # 混入响应式、省略号、居中 ├── base/ # 基础层全局元素 │ ├── _reset.scss # 样式重置 │ └── _typography.scss # 文本、标题、链接等 ├── layout/ # 布局层页面骨架 │ ├── _header.scss # 顶部导航 │ ├── _footer.scss # 页脚 │ └── _grid.scss # 栅格/容器宽度 ├── components/ # 组件层可复用模块 │ ├── _buttons.scss │ ├── _forms.scss │ └── _cards.scss └── pages/ # 页面层单页定制样式 └── _home.scss这个分层的思想是越底层的文件越不依赖其他层越上层的文件越具体。abstracts层几乎不产生实际 CSS 规则只是提供变量和混入base层格式化全局元素layout层搭出页面骨架components层放可复用组件pages层只放某个页面私有的覆盖。这样改一个按钮只需要打开components/_buttons.scss调一个全站主色只需要改abstracts/_variables.scss。3.2 逐个创建Partial文件我挑几个关键文件展开方便你直接照着写。先看abstracts/_variables.scss我习惯把颜色、字体、间距、断点全部收敛到这里并且用语义化命名而不是颜色名词$color-primary: #2a6df4; $color-primary-dark: #1c4fd1; $color-text: #222; $color-text-muted: #888; $color-border: #e5e7eb; $color-danger: #e5484d; $font-family-base: PingFang SC, Microsoft YaHei, sans-serif; $font-size-base: 14px; $font-size-sm: 12px; $font-size-lg: 18px; $spacing-unit: 8px; $spacing-sm: $spacing-unit; // 8px $spacing-md: $spacing-unit * 2; // 16px $spacing-lg: $spacing-unit * 3; // 24px $breakpoint-sm: 576px; $breakpoint-md: 768px; $breakpoint-lg: 992px;注意$spacing-md这类变量是基于基础单位用乘法算出来的而不是再写一个魔法数字这样全站间距会非常统一。再看abstracts/_mixins.scss我通常会放几个高频的混入mixin flex-center { display: flex; align-items: center; justify-content: center; } mixin ellipsis($line: 1) { if $line 1 { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } else { display: -webkit-box; -webkit-line-clamp: $line; -webkit-box-orient: vertical; overflow: hidden; } } mixin respond-to($breakpoint) { if $breakpoint sm { media (min-width: 576px) { content; } } else if $breakpoint md { media (min-width: 768px) { content; } } else if $breakpoint lg { media (min-width: 992px) { content; } } }base/_reset.scss里不只是粗暴的* { margin: 0 }。我比较喜欢设置全局盒模型*, *::before, *::after { box-sizing: border-box; } html { -webkit-text-size-adjust: 100%; } body { margin: 0; font-family: $font-family-base; font-size: $font-size-base; line-height: 1.6; color: $color-text; background-color: #fff; } img { display: block; max-width: 100%; } a { color: $color-primary; text-decoration: none; }这里用到了variables里的变量所以这个文件必须放在use abstracts/variables之后引入否则编译会报变量未定义。Sass 对引入顺序的敏感度和普通 HTML 里 link CSS 的顺序敏感度是一样的。base/_typography.scss里我会放一些全局文本工具类.text-muted { color: $color-text-muted; } .text-deleted { text-decoration: line-through; color: $color-text-muted; } .text-ellipsis { include ellipsis; }这里把 CSS 里的删除线这类修饰需求也放进了工具类用的时候类名即语义不用再去记忆一连串的text-decoration属性。很多团队还会扩展出text-center、text-left之类按需加就行。components/_buttons.scss我一般配合 BEM 命名来写.btn { display: inline-flex; align-items: center; justify-content: center; padding: $spacing-sm $spacing-md; border: 1px solid transparent; border-radius: 4px; font-size: $font-size-base; line-height: 1; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; --primary { background-color: $color-primary; color: #fff; :hover { background-color: $color-primary-dark; } } --ghost { background-color: transparent; border-color: $color-border; color: $color-text; } }Sass 的嵌套语法在这里比普通 CSS 舒服很多--primary最终会编译成.btn--primary:hover会编译成.btn--primary:hover。嵌套层级不要超过三层否则编译出来的选择器过深性能和维护性都会受影响。layout/_header.scss和pages/_home.scss我就不全部贴了思路一致layout 里放顶部导航的容器、logo、菜单排列pages 里放首页特有的 banner 或区块样式。值得提醒的是pages 里的选择器尽量用带前缀的类名比如.home-banner而不是直接在#home里写一堆 ID 级选择器否则优先级失控后非常难覆盖。ID 选择器在组件里也一样要尽量避免理由我会在第 4 章展开。3.3 入口文件main.scss的聚合顺序有了这些 Partialmain.scss要做的就是把它们按顺序聚到一起use abstracts/variables; use abstracts/mixins; use base/reset; use base/typography; use layout/grid; use layout/header; use layout/footer; use components/buttons; use components/forms; use components/cards; use pages/home;顺序的规律是被依赖的放前面公共的放前面具体的放后面。因为最终输出 CSS 时相同优先级的规则靠后的会覆盖靠前的。变量和混入放在最前面是硬性要求编译期就要先有定义base放前面可以保证基础的页面样式先行layout和components复用basepages放在最后用来做页面级的覆盖。这样从上往下读就是一个基础越来越具体的阅读路径出问题的时候定位也快。3.4 编译命令与自动化配置命令行直接敲是最快的验证方式npx sass styles/main.scss dist/main.css --styleexpanded跑完以后去dist目录看看应该只有一个main.css文件。Partial 不会出现在这里这就是拆文件但不增加产物文件的证据。项目里我更推荐把它固化在package.json的scripts里{ scripts: { sass: sass styles/main.scss dist/main.css --styleexpanded, sass:watch: sass --watch styles/main.scss:dist/main.css --stylecompressed --source-map } }如果项目用的是 Vite就简单很多直接装好sass之后在样式文件里use这些 Partial构建工具会自动处理编译连命令都不用写。Webpack 则通常借助sass-loader。还有一种偷懒但很实用的配置方式在构建工具里设置additionalData把abstracts里的变量自动注入到每个 scss 文件这样组件 Partial 里就不用每个都写use abstracts/variables。不过要注意additionalData只适合注入变量和混入这类不产生实际 CSS 的内容如果把真正会输出规则的样式也塞进去会让每个文件都带上重复代码。4. 进阶技巧与踩坑记录Partial真正落地时要注意的事4.1 变量语义化命名与Partial的粒度控制变量命名一定要语义化。我见过有人把变量起成$blue、$dark-blue、$red后来设计体系一改发现全局搜替换根本不可控。换成$color-primary、$color-danger、$color-text-muted之后含义一目了然而且跟设计稿的命名规范可以直接对齐。Partial 的粒度也要把握。一个文件只有五六行就完全没必要单开文件可以合并进相关的 Partial。我个人的标准是一个 Partial 至少在 30 行以上并且内部聚焦在同一类事物上。如果文件行数太多比如超过 300 行就要考虑是不是内部可以继续拆分。这个阈值不用纠结重点是改样式的时候能快速定位而不是文件越多越好。4.2 组件Partial的边界怎么划组件边界的问题最容易出的洋相是拆出来的组件其实只在一个页面用过一次。我的判断标准很简单这个模块有没有被两个以上页面复用如果还没有先放在pages对应文件里等到第二次复用的时候再抽到components文件夹。过早抽象其实比不抽象更痛苦因为你得凭直觉设计它的接口和可覆盖点。另外不要在组件 Partial 里使用 ID 选择器。ID 的优先级太高组件一旦被使用两次第二个实例里那些理所当然的 ID 规则就会让样式变得不可预测。类名才是组件的正确粒度。这个思路跟原子化 CSS 的差异在于组件 Partial 仍然是语义化的只是内部样式集中管理如果你把每个工具类都拆成一个文件甚至一个类那已经走向另一种风格不在本篇讨论范围内。4.3 五个真实踩坑记录我把自己实际踩过、也看同事踩过的几个典型问题列出来每一个都很具体。第一下划线写漏。刚开始用 Partial 的时候我把_header.scss建成了header.scss结果每次编译dist里都多一个header.css。项目文件一多产物目录里躺着一堆没用的 CSS首屏还被迫多加载了好几个文件排查了半天才发现是命名问题。解决办法就是上文说的编译完去产物目录检查多了哪个文件就是哪个文件没加下划线。第二use的命名空间拿不到变量。我在main.scss里写了use variables然后在组件 A 里直接写$primary-color结果报undefined variable。原因是我在组件 A 里没有引入 variables或者引入了但忘了带命名空间。正确写法是variables.$primary-color或者在组件 A 里也写一句use abstracts/variables并且用别名。为减少这种摩擦我一般给常用的变量文件加别名use abstracts/variables as *这样就能直接写$primary-color但这会牺牲命名空间隔离大团队项目要慎重。第三import和use混用。老项目里如果已经有了成片的import新代码再混入use很容易出现变量找不到、样式被重复输出这些诡异问题。最稳妥的方式是在一次重构里统一改成use不要长期并存。第四循环依赖。假设_buttons.scss引用了_mixins.scss而_mixins.scss又引用了_buttons.scss里的变量Sass 编译时会直接报Circular dependency错误。解决办法是把公共的变量、混入单独放在abstracts层里禁止跨层反向依赖。abstracts不依赖业务组件这个规矩要立住。第五相对路径漂移。组件文件从styles/components/移到styles/shared/components/之后里面写的use ../abstracts/variables就会失效编译报cant find stylesheet。当项目层级调整比较频繁时可以考虑在构建配置里设置includePaths把这些公共目录注册成绝对查找路径引用时直接写use abstracts/variables不用再数../。4.4 团队协作时的文件命名与提交规范如果多人协作Partial 这套东西最好配几条简单的约定否则每个人凭自己喜好拆最后还是乱。文件名统一用小写加连字符kebab-case比如_main-nav.scss而不要用驼峰_mainNav.scss或首字母大写_MainNav.scss。这能让文件在资源管理器里排序稳定也避免在一些对大小写敏感的操作系统之间同步时出现冲突。每个 Partial 顶部可以加个简短注释说明这个文件管什么、依赖了哪些变量或混入。比如// 顶部导航栏样式 // 依赖abstracts/variables、abstracts/mixins这个注释对后来接手的人非常友好。编译产物dist里的 CSS一律进.gitignore不要提交到仓库否则每次编译都会制造大量 diff。遇到样式问题先在源文件里找不要去产物文件里改。最后再分享一个我用了很久的小技巧在main.scss的最后面放一个临时调试区块用注释包起来专门给紧急热修用。修完问题之后再在对应的 Partial 文件里补上正式实现把临时区块删掉。这样可以保证线上问题先被按住同时不让临时补丁永远沉淀在工程里。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →