CSS 变量与预处理器:Sass、Less 与自定义属性实战

2024-08-30 前端

从预处理器变量到 CSS 自定义属性,再到二者的协同作战——这是一份全面的前端样式工程化指南。

目录

  1. CSS 自定义属性(CSS Variables)
  2. Sass/SCSS 深度指南
  3. Less 对比学习
  4. PostCSS 生态
  5. 预处理器 vs CSS 变量 选型指南

1. CSS 自定义属性(CSS Variables)

1.1 声明与使用

CSS 自定义属性(Custom Properties)由 CSS 规范原生定义,以 -- 作为前缀声明,通过 var() 函数读取。

基本语法:

/* 声明 */
:root {
  --primary-color: #3498db;
  --spacing-base: 8px;
  --font-size-large: 1.5rem;
}

/* 使用 */
.button {
  background-color: var(--primary-color);
  margin: var(--spacing-base);
  font-size: var(--font-size-large);
}

var() 函数接受两个参数:

.element {
  /* var(<自定义属性名>, <回退值>) */
  color: var(--undefined-var, #333);   /* fallback 生效:#333 */
  background: var(--primary-color, blue);
}

回退值可以嵌套使用:

.element {
  font-family: var(--font-primary, var(--font-fallback, sans-serif));
}

⚠️ 注意: var() 的 fallback 不支持多个值的简写属性分段回退。例如以下写法是错误的:

/* ❌ 错误:var() 内不能包含多值回退中的部分值 */
.border {
  border: var(--border-width, 1px) solid var(--border-color, #000);
  /* 如果 --border-width 未定义,只会得到 "1px solid",不会自动补 solid */
}

1.2 作用域与继承

CSS 变量的作用域遵循 CSS 级联规则——变量在离元素最近的作用域中生效。

:root {
  --accent: #e74c3c;       /* 全局变量 */
}

.card {
  --accent: #2ecc71;       /* 局部变量,覆盖 .card 及其子元素 */
  --radius: 8px;           /* 仅 .card 作用域内可见 */
  border-radius: var(--radius);
}

.card-title {
  color: var(--accent);    /* 继承 .card 的值,即 #2ecc71 */
}

.card-footer {
  --accent: #f39c12;       /* 更具体的局部覆盖 */
  --radius: 4px;           /* 仅在 .card-footer 内 */
  color: var(--accent);    /* #f39c12 */
}

继承链示例:

<div class="parent" style="--level: 1;">
  <div class="child" style="--level: 2;">
    <div class="grandchild">
      <p>Level: var(--level)</p>   <!-- 继承 .child 的值:2 -->
    </div>
  </div>
</div>

变量的继承特性使其非常适合设计系统中的主题令牌管理:

:root {
  /* 基础调色板 */
  --color-primary-100: #d6eaf8;
  --color-primary-500: #3498db;
  --color-primary-900: #1a5276;

  /* 语义化令牌,引用基础调色板 */
  --color-bg-primary: var(--color-primary-100);
  --color-text-link: var(--color-primary-500);
  --color-border-focus: var(--color-primary-900);
}

1.3 与预处理器变量的区别

这是整篇文章的核心比较,理解二者的本质区别至关重要。

特性 CSS 自定义属性 Sass/Less 变量
解析时机 运行时(浏览器) 编译时
动态性 ✅ 可在运行时修改 ❌ 编译后固定
作用域 DOM 继承(级联) 词法作用域(大括号)
JavaScript 操作 ✅ 原生支持 ❌ 需额外工具
媒体查询 ✅ 可在查询内改变 ❌ 不能直接使用
动画过渡 ✅ 可与 transition/animation 配合 ❌ 编译后为固定值
类型 字符串(浏览器不校验类型) 有类型概念(数字、颜色、列表)
计算能力 需配合 calc() 任意表达式直接运算
回退机制 内置 var(, fallback) 无原生回退

代码对比:

// Sass/SCSS — 编译时确定
$primary: #3498db;
$padding: 16px;

.button {
  background: $primary;
  padding: $padding;
}
// 编译后:
// .button { background: #3498db; padding: 16px; }
/* CSS 自定义属性 — 运行时动态 */
:root {
  --primary: #3498db;
  --padding: 16px;
}

.button {
  background: var(--primary);
  padding: var(--padding);
}
/* 浏览器解析,--primary 可在任何时刻被修改 */

本质差异一句话: 预处理器变量是编译期的宏替换,CSS 自定义属性是运行时的动态值。

1.4 JavaScript 操作

CSS 变量最大的优势之一——可以通过 JavaScript 在运行时动态读写。

读取变量:

const root = document.documentElement; // :root
const styles = getComputedStyle(root);

const primary = styles.getPropertyValue('--primary-color').trim();
console.log(primary); // "#3498db"

// 读取任意元素的变量
const card = document.querySelector('.card');
const cardRadius = getComputedStyle(card).getPropertyValue('--radius').trim();

设置变量:

const root = document.documentElement;
root.style.setProperty('--primary-color', '#e74c3c');

// 批量更新主题
function applyTheme(theme) {
  const themeMap = {
    light: {
      '--bg': '#ffffff',
      '--text': '#333333',
      '--border': '#dddddd',
    },
    dark: {
      '--bg': '#1a1a2e',
      '--text': '#e0e0e0',
      '--border': '#333333',
    },
  };

  const vars = themeMap[theme];
  const root = document.documentElement;
  Object.entries(vars).forEach(([name, value]) => {
    root.style.setProperty(name, value);
  });
}

applyTheme('dark');

移除变量:

root.style.removeProperty('--primary-color');

1.5 动态主题切换实战(暗色模式)

完整的暗色模式实现是 CSS 变量实战的最佳场景。

方案一:通过 class 切换

方案二:配合 prefers-color-scheme 媒体查询(自动跟随系统)

:root {
  --bg: #ffffff;
  --text: #222222;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1a1a2e;
    --text: #e0e0e0;
  }
}

JavaScript 切换逻辑:

1.6 响应式变量(在媒体查询中改变变量值)

CSS 变量可以在媒体查询中重新赋值,从而实现基于视口尺寸的样式变化——这是预处理器变量无法做到的。

更复杂的场景:基于容器查询(Container Queries)

.card-container {
  container-type: inline-size;
}

@container (max-width: 400px) {
  .card {
    --card-layout: vertical;
    --card-image-height: 200px;
    --card-padding: 12px;
  }
}

@container (min-width: 401px) {
  .card {
    --card-layout: horizontal;
    --card-image-height: 100%;
    --card-padding: 24px;
  }
}

.card {
  display: flex;
  flex-direction: var(--card-layout, horizontal);
  padding: var(--card-padding);
}

1.7 计算:calc() + var() 结合

CSS 变量是纯文本值,计算必须通过 calc() 函数完成。

1.8 @property —— 注册类型化自定义属性

@property 是 CSS Houdini 的一部分,允许开发者注册具有类型的 CSS 自定义属性,支持初始值、继承行为,并且让变量可以被 CSS 动画和过渡作用。

基本语法:

@property --rotation-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@property --color-start {
  syntax: '<color>';
  inherits: true;
  initial-value: #ff6b6b;
}

@property --color-end {
  syntax: '<color>';
  inherits: true;
  initial-value: #4ecdc4;
}

@property --border-radius {
  syntax: '<length-percentage>';
  inherits: false;
  initial-value: 0px;
}

支持的类型(syntax):

类型 描述 示例值
<length> 长度 10px, 2em
<number> 数字 42, 3.14
<percentage> 百分比 50%
<length-percentage> 长度或百分比 10px, 50%
<color> 颜色 #ff0, rgb(255,0,0)
<angle> 角度 45deg, 0.5turn
<time> 时间 2s, 500ms
<string> 字符串 "hello"
<custom-ident> 自定义标识符 ease-in-out
+ 空格分隔列表 <color>+
# 逗号分隔列表 <color>#

实战:让变量可动画

浏览器兼容性处理:


2. Sass/SCSS 深度指南

Sass(Syntactically Awesome Style Sheets)是目前最成熟、生态最完善的 CSS 预处理器,有 .sass(缩进语法)和 .scss(类 CSS 语法)两种格式。推荐使用 SCSS,它与 CSS 完全兼容,学习曲线更平缓。

2.1 变量

Sass 变量以 $ 开头,编译时替换为最终值。

// 基本变量
$primary-color: #3498db;
$font-stack: 'Inter', -apple-system, sans-serif;
$base-padding: 16px;
$enable-rounded: true;

// 变量引用变量
$link-color: $primary-color;
$link-hover-color: darken($primary-color, 10%);

.button {
  color: $primary-color;
  font-family: $font-stack;
  padding: $base-padding;

  @if $enable-rounded {
    border-radius: 8px;
  }
}

作用域规则:

// 全局变量
$global-color: #333;

.container {
  // 局部变量
  $local-color: #666;

  // 在嵌套作用域内重新声明全局变量(谨慎使用)
  $global-color: #111 !global;

  color: $local-color;   // #666
  border-color: $global-color;  // #111
}

// 编译后:
// .container { color: #666; border-color: #111; }

!default 标记:

!default 允许在变量已定义时保留原值,未定义时才赋值。这是设计系统和组件库的核心机制。

// _variables.scss — 默认配置
$primary: #3498db !default;
$danger: #e74c3c !default;
$font-size-base: 16px !default;
$border-radius: 4px !default;
$enable-shadows: false !default;

// _buttons.scss — 使用默认值
@import 'variables';

.button {
  background: $primary;
  font-size: $font-size-base;
  border-radius: $border-radius;

  @if $enable-shadows {
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  }
}

// app.scss — 覆盖默认值(必须在 import 前定义)
$primary: #e67e22;
$enable-shadows: true;
@import 'buttons';
// 最终 $primary 为 #e67e22,而非 #3498db

2.2 嵌套与 & 父选择器

Sass 嵌套大幅减少重复书写父选择器。

嵌套属性:

.card {
  // 嵌套属性的命名空间
  font: {
    family: 'Inter', sans-serif;
    size: 16px;
    weight: 600;
  }

  margin: {
    top: 10px;
    bottom: 20px;
  }

  background: {
    color: #f5f5f5;
    image: url('bg.png');
    size: cover;
  }
}

// 编译后:
// .card { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 600; margin-top: 10px; margin-bottom: 20px; background-color: #f5f5f5; background-image: url('bg.png'); background-size: cover; }

2.3 混合(Mixins)

@mixin / @include 是 Sass 最强大的功能之一,用于定义可复用的样式块。

无参数混入:

@mixin clearfix {
  &::after {
    content: '';
    display: table;
    clear: both;
  }
}

@mixin visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.container {
  @include clearfix;
}

.sr-only {
  @include visually-hidden;
}

带参数混入:

@content 占位符——块内容传递:

@content 允许混入接收调用者传入的额外样式块,实现类似「模板方法」模式。

2.4 函数(Functions)

Sass 函数与混入类似,但返回值而非样式块。

自定义函数:

常用内置函数速查表:

函数 作用 示例
darken($color, $amount) 变暗 darken(#3498db, 10%)
lighten($color, $amount) 变亮 lighten(#3498db, 10%)
saturate($color, $amount) 增加饱和度 saturate(#ccc, 30%)
desaturate($color, $amount) 降低饱和度 desaturate(#f00, 20%)
rgba($color, $alpha) 设置透明度 rgba(#3498db, 0.5)
mix($a, $b, $weight) 混合颜色 mix(red, blue, 50%)
map-get($map, $key) 取映射值 map-get($breakpoints, md)
map-has-key($map, $key) 键存在? map-has-key($breakpoints, xl)
nth($list, $n) 获取列表第 n 项 nth(10px 20px 30px, 2)
str-length($str) 字符串长度 str-length("hello")
unit($num) 获取单位 unit(10px) → px
unitless($num) 无单位? unitless(10) → true
percentage($num) 转百分比 percentage(0.5) → 50%

2.5 继承:@extend 与占位符选择器

@extend 允许一个选择器继承另一个选择器的样式。与混入不同,@extend 将选择器合并,生成更精简的 CSS。

占位符选择器 %:

占位符不会生成到 CSS 中,只有 @extend 它的选择器才会被编译。这是定义抽象基类的最佳方式。

@extend 注意⚠️:

  • 不要在嵌套很深的规则中使用 @extend,可能导致不可预期的选择器组合
  • 不要 @extend 不在同一组件内的选择器,违反样式隔离原则
  • 对于带参数的复用,使用 @mixin 而非 @extend
  • 推荐:只对占位符选择器使用 @extend

2.6 控制指令

Sass 提供了编程语言级别的控制流指令。

@if / @else if / @else:

@mixin button-variant($variant: 'primary') {
  $bg: null;
  $color: #fff;

  @if $variant == 'primary' {
    $bg: #3498db;
  } @else if $variant == 'success' {
    $bg: #2ecc71;
  } @else if $variant == 'warning' {
    $bg: #f39c12;
    $color: #333;
  } @else if $variant == 'danger' {
    $bg: #e74c3c;
  } @else {
    $bg: #95a5a6;
  }

  background: $bg;
  color: $color;

  &:hover {
    background: darken($bg, 10%);
  }
}

@each —— 遍历列表或映射:

@for —— 数值循环:

// 网格系统
$columns: 12;

@for $i from 1 through $columns {
  .col-#{$i} {
    flex: 0 0 calc(#{$i} / #{$columns} * 100%);
    max-width: calc(#{$i} / #{$columns} * 100%);
  }
}

// 字体尺寸
@for $i from 1 through 6 {
  h#{$i} {
    font-size: 2rem - ($i - 1) * 0.25rem;
  }
}

// @for ... to 不包含终值
@for $i from 1 to 4 {
  .mt-#{$i} { margin-top: $i * 4px; }
}
// 生成 .mt-1, .mt-2, .mt-3(没有 .mt-4)

@while —— 条件循环:

// 不太常用,但在某些算法中有用
$i: 1;
$scale: 12px;

@while $i <= 5 {
  .fs-#{$i} {
    font-size: $scale * $i;
  }
  $i: $i + 1;
}
// 生成 .fs-1 { font-size: 12px; } ... .fs-5 { font-size: 60px; }

2.7 模块化:@use 与 @forward

Sass 的现代模块系统(从 Dart Sass 1.23.0 起)替代了旧的 @import。

@use —— 加载模块:

@forward —— 转发模块:

@forward 将一个模块的成员"透传"给后续的 @use,常用于入口文件。

// _index.scss — 统一出口
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// app.scss — 入口文件
@use 'index' as *;
// 现在可以直接使用 _variables.scss、_mixins.scss 和 _functions.scss 中的所有成员
// 无需再单独 @use 它们

// 选择性转发
// _internal.scss
$-private-var: 42;           // 私有(不会被转发)
$public-var: 100;           // 公开

@forward 'internal' show $public-var;
// 只公开 $public-var,隐藏 $-private-var

// 转发表
@forward 'colors' as color-*;
// 将 colors 模块中所有成员重命名为 color-* 前缀

可见性控制:

// 私有成员(以 - 或 _ 开头,不会被 @use 外部访问)
$-internal-padding: 16px;
$_internal-mixin {
  // ...
}

// 公有成员
$public-primary: #3498db;

2.8 项目组织:7-1 模式

Sass 7-1 模式是业界最广泛使用的组织方式,适用于中大型项目。

入口文件 main.scss:


3. Less 对比学习

Less(Leaner Style Sheets)是另一款流行的 CSS 预处理器,以 JavaScript 实现,可以在 Node.js 或浏览器中运行。

3.1 变量与混入

变量:

// Less 使用 @ 定义变量
@primary: #3498db;
@secondary: #2ecc71;
@font-stack: 'Inter', Arial, sans-serif;
@base-padding: 16px;
@grid-columns: 12;

.button {
  color: @primary;
  font-family: @font-stack;
  padding: @base-padding;
}

// 懒加载(Lazy Loading)—— Less 的重要特性
@size: 20px;

.header {
  font-size: @size; // 24px(使用后面定义的值)
}

@size: 24px;

.footer {
  font-size: @size; // 24px
}

// Sass 中则是顺序解析,先定义的先使用

混入(Mixin)—— Less 无 @mixin 语法,选择器可直接作为混入:

3.2 嵌套与 &

与 Sass 非常相似:

.nav {
  display: flex;
  list-style: none;

  > li {
    margin: 0 8px;
  }

  a {
    text-decoration: none;
    color: @primary;

    &:hover {
      color: darken(@primary, 10%);
    }

    // 父选择器拼接
    &-link {
      padding: 8px;
    }
  }

  // 作用域内定义的变量
  @nav-padding: 16px;
  padding: @nav-padding;
}

3.3 Sass vs Less 核心差异

特性 Sass/SCSS Less
实现语言 Dart(原生)/ LibSass(已弃用) JavaScript
变量前缀 $ @
变量作用域 块作用域(先声明后使用) 块作用域(懒加载)
混入语法 @mixin / @include .class() 直接复用
条件 @if / @else 混合了 guard(when)
循环 @each / @for / @while each() 函数
函数 @function .mixin() 带返回值(Less 无函数关键词)
自定义函数 ✅ 原生支持 ❌ 依赖插件
模块系统 @use / @forward(现代) @import(旧)、@import (reference)
生态 极成熟(Bootstrap 5 等) 成熟(Bootstrap 3/4,Ant Design)
编译速度 Dart Sass 较快 Node 环境下原生速度
Map/列表操作 丰富(map-get, nth 等) 有限(each + lists)

Less 的条件守卫(Guarded Mixins):

// Less 使用 when 实现条件
.mixin(@size) when (@size > 10px) {
  font-size: @size;
  line-height: 1.5;
}

.mixin(@size) when (@size <= 10px) {
  font-size: @size;
  line-height: 1.2;
}

.text { .mixin(16px); }  // 第一个 mixin 生效

// 类型守卫
.color-mixin(@color) when (iscolor(@color)) {
  border-color: @color;
}

.color-mixin(@shadow) when (isruleset(@shadow)) {
  box-shadow: @shadow;
}

Less 的 @import 与命名空间:

// _variables.less
@primary: #3498db;
@danger: #e74c3c;

// main.less
@import 'variables';

// Less 可以通过包围来模拟命名空间
#namespace() {
  .button() {
    background: @primary;
    color: #fff;
  }
}

.btn {
  #namespace.button();  // 避免命名冲突
}

4. PostCSS 生态

PostCSS 不是预处理器,而是一个用 JavaScript 插件处理 CSS 的后处理工具链。它的核心理念是:你写标准 CSS,插件自动完成兼容性、优化、未来语法转换等工作。

4.1 核心插件

autoprefixer —— 自动添加厂商前缀:

/* 输入 */
.grid {
  display: grid;
  user-select: none;
  appearance: none;
  backdrop-filter: blur(10px);
}

/* 输出(含 autoprefixer) */
.grid {
  display: -ms-grid;
  display: grid;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

配置(browserslist):

// package.json
{
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "not op_mini all"
  ]
}
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      // 也可以在这里覆盖 browserslist
      overrideBrowserslist: ['last 2 versions', '> 1%'],
    }),
  ],
};

cssnano —— 极致压缩:

/* 输入 */
.card {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px 24px;
  margin: 8px 0;
  background-color: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  font-size: 16px;
  line-height: 1.5;
  color: #333333;
}

/* 输出(cssnano 压缩后) */
.card {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px 24px;
  margin: 8px 0;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  font: 16px/1.5 sans-serif;
  color: #333;
}

cssnano 的压缩类型:

preset 特点 适合场景
cssnano-preset-default 安全压缩,不改变 CSS 语义 生产环境(推荐)
cssnano-preset-advanced 激进压缩(可能破坏某些模式) 绝对确定语义时
cssnano-preset-lite 轻量压缩,速度最快 开发环境检查

postcss-preset-env —— 未来 CSS 语法转换:

将未来的 CSS 特性(Stage 0-4)转换为当前浏览器兼容的代码。

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-preset-env')({
      stage: 2,              // 默认 stage 2,生产推荐
      browsers: 'last 2 versions',
      autoprefixer: true,   // 内置 autoprefixer
      features: {
        'custom-properties': false,     // 不编译 CSS 变量(保留给浏览器)
        'nesting-rules': true,          // 启用嵌套
        'custom-media-queries': true,
        'gap-properties': true,
      },
    }),
  ],
};

PostCSS 嵌套 vs Sass 嵌套:

/* PostCSS 嵌套(postcss-nesting 或 postcss-preset-env) */
.card {
  background: #fff;

  & .title {
    font-size: 18px;
  }

  &:hover {
    transform: translateY(-2px);
  }

  @media (width >= 768px) {
    padding: 24px;
  }
}

4.2 完整 PostCSS 工作流

// postcss.config.js — 生产环境配置
module.exports = {
  plugins: [
    // 1. 解析未来 CSS
    require('postcss-preset-env')({
      stage: 2,
      features: {
        'nesting-rules': true,
        'custom-media-queries': true,
      },
    }),

    // 2. 其他后处理
    require('postcss-import'),               // 合并 @import
    require('postcss-assets')({              // 资源处理(图片尺寸等)
      loadPaths: ['src/images/'],
    }),

    // 3. 压缩(仅生产环境)
    ...(process.env.NODE_ENV === 'production'
      ? [require('cssnano')({
          preset: ['default', {
            discardComments: { removeAll: true },
            normalizeWhitespace: true,
          }],
        })]
      : []),
  ],
};

4.3 与预处理器的配合

PostCSS 通常不替代预处理器,而是在预处理器之后运行。

源文件(SCSS)  →  Sass 编译  →  CSS  →  PostCSS 处理  →  最终 CSS
                                          ↓
                                    autoprefixer
                                    cssnano
                                    postcss-preset-env

构建配置示例(Webpack):

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          'style-loader',        // 3. 注入到 DOM
          'css-loader',          // 2. 解析 CSS
          {
            loader: 'postcss-loader',  // 1b. PostCSS 后处理
            options: {
              postcssOptions: {
                plugins: [
                  'autoprefixer',
                  process.env.NODE_ENV === 'production' && 'cssnano',
                ].filter(Boolean),
              },
            },
          },
          'sass-loader',         // 1a. Sass 编译
        ],
      },
    ],
  },
};

5. 预处理器 vs CSS 变量 选型指南

5.1 使用场景对比表

场景 推荐工具 原因
设计系统令牌(颜色、字体、间距) CSS 变量 易于在运行时更新,支持暗色模式
动态主题切换 CSS 变量 + JS 运行时修改,无刷新即时生效
复杂计算(颜色运算、数学公式) 预处理器 内置 darken/lighten 等函数
响应式设计 CSS 变量 可在媒体查询中改变值
代码复用(Mixin) 预处理器 @mixin / @include 无可替代
循环生成(网格、工具类) 预处理器 @for / @each
组件级别的主题覆盖 CSS 变量 作用域继承,组件实例化
向后兼容(旧浏览器) 预处理器 CSS 变量需 postcss-preset-env polyfill
性能敏感场景 预处理器 编译时确定,无运行时开销
动画 + 变量 CSS 变量 + @property 支持过渡和关键帧动画
嵌套选择器 预处理器或 PostCSS 减少重复
组织大型项目 预处理器 + 7-1 模式 模块化系统成熟

5.2 同时使用的最佳实践

CSS 变量与预处理器不是二选一,而是互补工具。 最佳方案是同时使用二者,各取所长。

原则 1:预处理器负责编译时任务,CSS 变量负责运行时任务

// _variables.scss — Sass 变量(编译时)
$color-primary: #3498db;
$color-success: #2ecc71;
$color-danger: #e74c3c;
$border-radius-sm: 4px;
$border-radius-md: 8px;
$border-radius-lg: 16px;

// _tokens.scss — CSS 变量(运行时)
:root {
  --color-primary: #{$color-primary};
  --color-success: #{$color-success};
  --color-danger: #{$color-danger};

  --border-radius-sm: #{$border-radius-sm};
  --border-radius-md: #{$border-radius-md};
  --border-radius-lg: #{$border-radius-lg};

  // 计算后的值
  --spacing-xs: #{spacing(0.5)};
  --spacing-sm: #{spacing(1)};
  --spacing-md: #{spacing(2)};
  --spacing-lg: #{spacing(4)};
  --spacing-xl: #{spacing(8)};
}

原则 2:预处理器做静态生成,CSS 变量做动态覆盖

原则 3:Sass 生成实用工具类,CSS 变量控制细节

5.3 最终决策框架

面对一个新项目,需要根据项目特性选择工具:

推荐组合(2024+):

场景 推荐方案
新项目(标准) Sass (Dart Sass) + PostCSS + CSS 变量
新项目(激进) PostCSS-preset-env + CSS 变量 + @property
组件库 / 设计系统 Sass (编译时生成) + CSS 变量 (运行时主题) + PostCSS (兼容)
遗留项目迁移 保留现有预处理器,逐步引入 CSS 变量
演示 / 原型 纯 CSS 变量 + CDN PostCSS

清单:检查你同时使用预处理器和 CSS 变量时是否正确:

  • [ ] Sass 变量用于编译时的循环、生成、计算
  • [ ] CSS 变量用于运行时的主题、覆盖、响应式
  • [ ] 用 #{$var} 插值将 Sass 变量的值注入 CSS 变量
  • [ ] Sass Mixin 生成模板代码,CSS 变量控制实例化细节
  • [ ] 颜色等复杂运算在 Sass 中完成,结果以 CSS 变量暴露
  • [ ] 组件库提供 Sass API($primary !default)和 CSS API(--primary)
  • [ ] PostCSS 放在构建管道的最后一步进行兼容和优化

结语

CSS 自定义属性和预处理器的关系不是替代,而是互补。预处理器擅长编译时的代码生成、循环、计算和组织,CSS 变量擅长运行时的动态更新、继承和主题系统。

用预处理器构建系统的骨架,用 CSS 变量赋予系统生命。 当你能熟练地在 Sass 中 @each 生成工具类,同时用 CSS 变量让用户在运行时切换主题时,你才真正掌握了现代 CSS 工程化的精髓。

未来的方向是原生 CSS 越来越强大——@property、@layer、nesting、container queries 都在缩小与预处理器的差距。但就目前而言,Sass/SCSS + CSS 变量 + PostCSS 的组合仍然是生产项目最成熟、最平衡的选择。

进一步阅读:


本文发布于 2024-08-30,文中涉及的技术版本以撰写时为准。CSS 和 Sass 均在积极演进中,建议关注各项目官方文档获取最新信息。