资讯详情

用Sass循环批量生成颜色与间距辅助类,告别手写CSS工具类

📅 2026/9/11 11:53:34 | 华诺云谱 👁 阅读
用Sass循环批量生成颜色与间距辅助类,告别手写CSS工具类
写CSS写多了以后你会发现一个特别磨人的场景设计稿里定了一套颜色规范比如主色、成功、警告、危险、信息再加上深浅浓淡好几档间距规范从 4px 到 48px 甚至更大8 档、10 档然后你打开编辑器开始了复制粘贴之旅——.text-primary { color: #2196F3; }再来一个.text-success { color: #4CAF50; }接着.text-warning、.text-danger、.text-info搞完文字色还要搞背景色背景色搞完还有边框色间距就更不用说了margin和padding各来一套上右下左四个方向再加两个轴向一套下来几十个类名手指都快抽筋了。这还不算完。下个月产品经理过来说“颜色再调整一下间距规范改成 4 的倍数”你就要回去把这一坨手写的辅助类重新改一遍。改的时候还得小心翼翼怕漏掉哪个又怕改错了影响别的地方。我早期做项目就是这么硬扛过来的直到后来完整用了一遍 Sass 的循环语法才意识到这种重复劳动完全可以交给代码去生成。这篇东西就是把我实际项目里用 Sass 循环批量生成颜色、间距辅助类的思路和方案完整写出来。适合正在写组件库、搭 Design System或者单纯受够了手写一堆工具类的朋友参考。内容不算难但非常实用属于“今天看完今天就能用”的类型。1. 整体思路拆解为什么用 Sass 循环生成辅助类1.1 辅助类到底解决了什么问题辅助类Utility Class在很多项目里也叫工具类、功能类它们干的事情很纯粹一个类名只负责一条 CSS 声明或者一组高度相关的声明。比如.mt-16就是margin-top: 16px;.text-center就是text-align: center;。这种方式的优点在于你在写 HTML 的时候可以直接在标签上堆类名不用为一个 margin 值去单独写一个样式类也不用为了调间距反复切到样式表里改。页面的样式结构简单清晰而且辅助类的值是固定在规范里的不会出现设计师规定间距只从 4、8、12、16 里面选结果你写了个 13px 出来的尴尬情况。说白了辅助类就是在帮你统一风格、减少重复、加快开发速度。但问题也来了。颜色有文字色、背景色、边框色每个色系下又有深浅变化间距有 margin 和 padding每个又分 top 方向、right 方向、bottom 方向、left 方向、x 轴、y 轴。把这些维度全部组合起来数量就非常可观。一个中等规模的项目如果全部手写动辄就是几百行极其相似的代码。人肉去写这种代码既不优雅也很容易出错。1.2 方案选型为什么用循环而不是手写或 CSS 变量面对这个场景其实有三条路可以走。第一条路就是手写。最简单但缺点是显而易见的量大、枯燥、难维护、容易看漏。特别是中途要新增一个颜色或调整一个间距刻度你得在所有相关的位置同步修改漏了一个页面表现就不一致。第二条路是用 CSS 变量加计算。比如定义一堆--spacing-4: 4px;的变量然后再配合calc()或者var()去生成样式。不过 CSS 变量本身不能直接生成类名它只能在值层面帮你统一管理。也就是说你依然要写margin-top: var(--spacing-4);写多了依然是一堆重复代码只是比硬编码好一些。第三条路就是今天要说的 Sass 循环。Sass 是一种 CSS 预处理器它让你可以在写样式的时候使用变量、嵌套、混入、函数、循环这些编程才有的能力。关键是它可以在构建阶段就帮我们把循环里的每一次迭代展开成真正的 CSS 类名。这样你只需要维护一份色板变量和一份间距刻度定义剩下的大量重复类全部由 Sass 自动生成。我用下来最大的感受是Sass 循环方案的核心优势不在于“少打字”而在于“单一事实来源”。你的颜色、间距规范只定义一次生成的辅助类、数量、命名风格完全由代码决定。后面想改全局的间距基准改一行变量所有相关类一次性更新不会再出现东漏一个西漏一个的问题。1.3 这个方案适合用在哪些场景不是所有项目都需要搞一整套辅助类。我个人的判断标准是只要你的项目里出现“重复的、有规律的、且跟设计规范强绑定”的样式就值得用这个思路。最典型的是这么几类场景组件库或 UI 框架的开发阶段需要输出大量固定规范的辅助类。中大型业务项目设计规范明确间距、颜色都是固定刻度。快速迭代的官网、活动页、后台管理系统需要频繁用到工具类来布页面。如果你只是一个几页的静态页面总共用不到几个颜色和间距那确实没必要上 Sass 循环手写反而更直接。但在那些样式复用频率极高的项目里这个方案会给你省下非常多的时间。2. Sass 循环基础先搞清楚 each、for、while 的区别2.1 三种循环语法各自擅长的事开始写生成代码之前得先把 Sass 里三个循环指令的脾气摸清楚。它们各有各的主场。each是循环一个列表或者一个 Map 里的每一项。这种场景最贴合“有一组颜色/间距值要为每一项生成一段代码”的诉求。比如你有$colors: (primary: #2196F3, success: #4CAF50);这样一个 Map直接用each $name, $color in $colors { ... }就能把主色和成功色都遍历到非常干净。for是按数字范围循环适合“间距刻度是 0、4、8、16……这种规律的递增序列”的场景。比如从 0 循环到 100步长为 4就可以用for $i from 0 through 25 { ... }然后在里面算$i * 4得到实际间距值。要注意 Sass 里through是包含结束值to是不包含这两个用混了生成的类数量会差一个很容易踩坑。while是条件式循环只要条件成立就一直跑。这个在辅助类场景里用得相对少但有一个地方特别合适比如你要生成一个宽度比例列表2%、4%、8%、16%、32%、64%…… 这种翻倍增长的序列用for不太好写而while可以轻松搞定。2.2 插值语法和符号的配合有了循环指令还不够你还得会用插值语法。在 Sass 里类名如果包含变量必须通过#{$name}这种插值方式拼进选择器里。直接写.text-$name是无效的因为它没法被识别成一个类名。这在辅助类生成里是核心中的核心因为你要动态生成的就是类名本身。此外还有一个进阶技巧利用符号来生成带前缀的复合类名。比如你想在循环里生成.text-primary:hover这样的伪类样式可以这样写each $name, $color in $colors { .text-#{$name} { color: $color; :hover { color: darken($color, 8%); } } }这里的会自动被替换成外层的选择器也就是.text-primary于是编译结果就是.text-primary:hover。这个写法在生成带交互状态的颜色类时特别好用不用手写一堆伪类样式。2.3 用函数提升复用性循环中经常要配合函数使用。Sass 内置了很多颜色函数比如lighten()、darken()、mix()、rgba()这些在处理色板深浅色的时候非常有用。我个人最常用的是把“根据基础色生成一组辅助类”的逻辑封装成一个 mixin或函数这样主色、成功色、警告色都能复用同一套生成逻辑代码颜值也会高不少。这一段的重点总结成一句话each管名字到值的映射for管数字规律的批量生成while管不规律但有条件的序列插值和是拼接选择器的关键工具。这几样组合起来后面批量生成辅助类就没什么障碍了。3. 核心实操批量生成颜色辅助类3.1 先把色板变量设计好颜色辅助类的第一步是把色板定义好。我建议用 Sass 的 Map 来定义因为 Map 能同时保存颜色的名字和值循环的时候非常方便。这里给你一个我在项目里常用的基础模板$colors: ( primary: #2196F3, success: #4CAF50, warning: #FF9800, danger: #F44336, info: #00BCD4, light: #F5F5F5, dark: #212121 );关于颜色命名我强烈建议使用语义化命名不要用blue、red这种表面描述。因为今天项目的主色是蓝色明年改版可能就换成绿色了如果你类名叫.text-blue明年就得追到所有用到它的地方改类名而如果叫.text-primary只需要把变量里的值换成绿色其他一切照旧。这是我在实际项目中踩过坑之后才深刻体会到的。3.2 each 一键生成所有颜色类色板定义好之后生成代码就非常简单了。比如我需要生成背景色、文字色、边框色三个系列可以这么写each $name, $color in $colors { .bg-#{$name} { background-color: $color; } .text-#{$name} { color: $color; } .border-#{$name} { border-color: $color; } }就这么几行编译之后会生成多少类呢7 个颜色乘 3 个系列一共 21 个类名。如果手写的话这 21 行代码每一行都要你亲自敲上去现在三行循环解决。这里我推荐再加一个点把 hover 态也一并生成避免很多“鼠标放上去颜色要加深一点”的细节需求。用前面提到的darken()函数就可以了each $name, $color in $colors { .text-#{$name} { color: $color; :hover { color: darken($color, 8%); } } }不过要注意darken()对大部分彩色系的颜色效果不错但对纯黑、纯白这类颜色会有点尴尬。比如darken(#fff, 8%)之后会得到偏灰的颜色这不一定符合预期。所以这个技巧适合用在彩色系上黑白灰可以单独处理。3.3 生成透明度变体同色系深浅辅助类除了基础的整块颜色很多设计系统还需要同一色系下的不同透明度变体。比如primary色的 10% 透明度背景用于表格 hover、标签页背景danger色的 20% 透明度背景用于警示提示条的底色。这种场景可以通过一个嵌套 Map 来实现。色板结构改成这样$colors: ( primary: ( base: #2196F3, light: #BBDEFB, dark: #1976D2 ), ... );然后循环嵌套循环each $name, $variants in $colors { each $variant, $color in $variants { .bg-#{$name}-#{$variant} { background-color: $color; } .text-#{$name}-#{$variant} { color: $color; } } }这样生成出来的类名就是.bg-primary-light、.bg-primary-dark、.text-primary-light这种风格非常直观。实际项目中我还经常用rgba($color, .1)这种方式生成“低透明度的渐变层”不过这个因人而异看你的设计规范怎么定了。4. 核心实操批量生成间距辅助类4.1 间距刻度怎么定间距辅助类比颜色类要更“数字化”一些。我一般先把间距刻度定义成一个列表而不是硬算。这样做的原因是真实设计稿里的间距很少是纯粹的等差数列更多的时候是 4、8、12、16、24、32、48 这种一眼看过去就“有点讲究”的序列。$spacings: ( 0: 0px, 1: 4px, 2: 8px, 3: 12px, 4: 16px, 5: 24px, 6: 32px, 7: 48px, 8: 64px );这里用数字做 key用实际像素值做 value。类名设计成.m-4、.mt-4、.px-4这样既能表达含义写起来也短HTML 里堆类名的时候不累赘。4.2 each 或 for 循环生成 margin 和 padding如果你喜欢用each直接遍历上面这个 Map 就行。但是如果间距刻度本身就是规律的倍数关系我更推荐用for配合一个缩放变量来生成。比如间距基准是 4px最大到 64px那就是从 0 到 16 循环每次乘以 4$spacing-base: 4px; $spacing-steps: 16; for $i from 0 through $spacing-steps { $value: $i * $spacing-base; .m-#{$i} { margin: $value; } .p-#{$i} { padding: $value; } .mt-#{$i} { margin-top: $value; } .mr-#{$i} { margin-right: $value; } .mb-#{$i} { margin-bottom: $value; } .ml-#{$i} { margin-left: $value; } .pt-#{$i} { padding-top: $value; } .pr-#{$i} { padding-right: $value; } .pb-#{$i} { padding-bottom: $value; } .pl-#{$i} { padding-left: $value; } .mx-#{$i} { margin-left: $value; margin-right: $value; } .my-#{$i} { margin-top: $value; margin-bottom: $value; } .px-#{$i} { padding-left: $value; padding-right: $value; } .py-#{$i} { padding-top: $value; padding-bottom: $value; } }这段代码跑一遍从.m-0到.m-16一共生成了 13 个系列乘以 17 个刻度两百多个类。如果全部手写那画面我都不敢想。而且这套类的命名规则非常直觉化mt就是 margin-toppx就是水平方向的 padding只要是写过两天 CSS 的人看到类名基本就能猜出含义。4.3 负边距怎么处理间距辅助类还有一个容易被忽略但相当实用的场景负边距。比如很多布局里需要用margin-left: -20px来做视觉上的对齐微调或者做横向滚动区域的偏移修正。这时候如果能直接用类名.m-n4表达“margin: -16px”效率会高很多。负边距的生成可以在原有循环的基础上加一点处理。比如用同一个刻度列表循环时判断一下当前值是否为 0然后多生成一组负数类for $i from 0 through $spacing-steps { $value: $i * $spacing-base; .m-#{$i} { margin: $value; } .mt-#{$i} { margin-top: $value; } if $i 0 { .m-n#{$i} { margin: -$value; } .mt-n#{$i} { margin-top: -$value; } .mx-n#{$i} { margin-left: -$value; margin-right: -$value; } } }这里用if $i 0把负值限定在非零刻度上避免生成margin: -0px这种毫无意义的代码。实际项目中负边距虽然用得不频繁但一旦用到手写起来又很容易写错符号所以预生成好反而是更安全的选择。5. 进阶玩法响应式前缀、状态类、尺寸辅助类5.1 给辅助类加响应式断点辅助类项目做大了之后一个很常见的需求是响应式。比如在手机上间距要小一点桌面上间距要大一点手机端某个元素居左平板以上居右。如果你只有.mt-4这种不带断点的类就只能再写媒体查询覆盖非常痛苦。Sass 循环完全可以配合媒体查询来生成带断点前缀的辅助类。思路是定义一个断点 Map然后循环断点在每一层断点内部再循环一次间距或颜色$breakpoints: ( sm: 576px, md: 768px, lg: 992px, xl: 1200px ); each $bp-name, $bp-width in $breakpoints { media (min-width: $bp-width) { for $i from 0 through $spacing-steps { $value: $i * $spacing-base; .#{$bp-name}\:mt-#{$i} { margin-top: $value; } .#{$bp-name}\:text-center { text-align: center; } } } }这样生成的类名会是.md:mt-4、.lg:mt-8。在 HTML 里写div classmt-4 md:mt-8意思是默认 16px屏幕宽度超过 768px 变成 32px。这个模式现在很多组件库都在用比如 Tailwind CSS 就是这种思路的集大成者。稍微注意一下类名里的冒号需要用反斜杠转义也就是.#{$bp-name}\:mt-#{$i}这个细节不处理Sass 编译会直接报错。5.2 用 while 生成非线性的尺寸类前面提到while适合不规则规律序列这里举一个实际例子。假设你要生成一组宽度百分比辅助类25%、50%、75%、100%这个用for也能做。但如果是 5%、10%、20%、40%、80% 这种每次都翻倍的序列用for就得额外算指数非常别扭。这时while就很好用$value: 5; while $value 80 { .w-#{$value} { width: percentage($value / 100); } $value: $value * 2; }这个循环会依次生成.w-5、.w-10、.w-20、.w-40、.w-80这几个类。Sass 的percentage()函数会把小数转成百分比形式输出.w-5 { width: 5%; }。这种翻倍序列用其他循环写起来都会很绕但while几乎是天然为此设计的。5.3 按需生成避免输出体积膨胀用循环生成辅助类最大的隐藏风险是如果不加节制生成出来的 CSS 体积会非常大。有些类你可能整个项目就用了两次但它一样会原封不动地出现在最终的 CSS 文件里。控制体积的方法有两个。一种是用注释加标记配合 PurgeCSS、UnCSS 这类工具在打包阶段把没用到的类删掉。比如你可以在生成出来的类名前统一加一个标志然后让 PurgeCSS 去识别。另一种更直接的方式是控制循环条件。不要为了“万一以后用得上”而生成一堆全量组合。比如间距刻度最大到 64px你就不要生成到 200px颜色只有 7 个主色就没必要再额外生成 7 个高亮色、7 个浅面色。先把当下能确定的生成出来后面需要再加成本本来就很低因为循环代码改一行就行真正麻烦的是改 HTML 里用到的旧类名。6. 常见问题与排查技巧实录6.1 类名没生成出来或者输出和预期不一样最常遇到的情况是循环写完了编译也没报错但输出的 CSS 里就是找不到某个类。多半是循环条件写错了。比如for $i from 0 to 5和from 0 through 5的结果就不一样前者生成 0 到 4后者生成 0 到 5。差一个值对应的类名就会缺失。排查方法是打开编译后的 CSS 文件看一眼生成的最后一个类是多少。要是发现少了边界值直接去查你的through和to是不是用混了。另一个高频问题是插值语法写错比如写了$i而不是#{$i}编译出来的选择器就会变成.mt-0这样的固定类名或者直接编译失败。这个检查起来最简单看到类名不对就去查插值。6.2 类名冲突辅助类互相覆盖辅助类多到一定程度就会遇到一个非常现实的问题.mt-4和.px-4在同一条元素上出现时不会冲突但如果误用了.m-4和.mt-8在 CSS 优先级相同、HTML 类名顺序又不定的情况下最后应用哪个值就由样式表里谁写在后面决定。这是一个标准的面条式问题实际开发中非常难排查。我给项目的做法是规划好类名前缀。间距类统一用m、p开头并且严格要求同一个属性的类不要同时出现在一个元素上。此外可以给所有辅助类加一个统一的作用域类名开头比如.u-mt-4这样至少和业务样式区分开优先级排查的时候心理负担小很多。6.3 编译速度变慢项目大了以后Sass 编译速度确实会受循环次数影响。这个在几百个类的量级几乎无感但如果你生成几千个小类再叠加多套主题编译时间就会明显上升。一个有效的优化手段是减少嵌套层数。比如响应式断点循环里如果你在断点下再嵌套颜色循环、字体大小循环、间距循环编译很容易变慢。这个时候可以考虑把断点拆出去只对真正需要的类生成响应式版本而不是对每一个辅助类都补偿一套断点前缀。反正我实测下来按需生成比全量生成编译时间和文件体积都能降不少维护起来也更省心。6.4 不同 Sass 编译器的兼容性这里特别提醒一下Sass 现在有两种主要实现node-sass也就是 LibSass和dart-sass。LibSass 已经官方宣布停更虽然存量项目里还有很多人在用但它对某些新版语法支持不完整特别是一些 Map 函数和模块系统。我用dart-sass写循环从来没出过兼容性问题所以如果你是新项目直接上 Dart Sass 就好。如果项目里是用 Webpack 的话一般配置的是sass-loader它底层可以切换编译器。你只要把implementation字段指向sass这个包也就是 Dart Sass就可以无缝切换。切换之后循环语法基本通用但有个别函数在 LibSass 里行为不一样建议切完跑一遍全量回归测试。6.5 命名规范的统一最后聊一个“软性”但很重要的问题类名到底该怎么命名。命名规范不统一团队协作的时候就会很混乱。有人写.mt-16有人写.margin-top-16还有人写.mt16搜索和替换都麻烦。我个人的规范是属性缩写 方向缩写 刻度数字。margin 用 mpadding 用 ptop/right/bottom/left 分别用 t/r/b/lx/y 代表横轴和纵轴。颜色类则是bg、text、border开头后面接语义色名。这个规范在上面的示例代码里已经贯穿全程了你完全可以直接抄过去用。统一命名之后整个团队的代码阅读成本都会直线下降碰到辅助类相关的问题一眼就能看出它控制的是什么属性。这几年我维护过好几个带辅助类体系的前端项目最大的体会是辅助类这东西一旦用起来确实能提升开发效率但也别贪多。每多一个类都是给团队增加一份记忆负担和使用成本。Sass 循环真正帮我解决的是把“生成大量规则类”的重复劳动降到最低这样我反而有更多精力去思考到底哪些类值得生成哪些类最好不要出现。如果你正准备在项目里搭一套辅助类体系建议先从小而精的色板、间距开始跑通这套循环生成流程之后再慢慢扩展。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。