跳到主要内容
预计阅读 41 分钟

CSS篇 | SCSS与LESS

前言

原生 CSS 虽然在不断进化(CSS 变量、嵌套、@layer 等新特性),但在大型项目中,CSS 预处理器依然是不可或缺的工具。SCSS(Sass)和 LESS 是目前最主流的两个 CSS 预处理器,几乎每个前端项目都在用。

面试中,CSS 预处理器相关的问题通常不会特别深入,但会考察你的实际开发经验:

  • “你用过 SCSS 还是 LESS?它们有什么区别?”
  • “CSS 预处理器解决了什么问题?”
  • “什么是 Mixin?和 @extend 有什么区别?”
  • “PostCSS 和 SCSS 是什么关系?”
  • “CSS Modules 和 CSS-in-JS 了解吗?”

如果你只是会用变量和嵌套,面试时很难展现出深度。今天我们就把预处理器的核心能力和工程化方案理清楚。

诊断自测

在开始之前,试着回答以下问题:

1. SCSS 的变量用 $,LESS 的变量用 @,除此之外它们还有哪些核心区别?

2. 以下 SCSS 代码的编译结果是什么?

@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

.container {
  @include flex-center;
  height: 100vh;
}

3. @mixin@extend 各自的使用场景是什么?哪个会导致样式膨胀?

点击查看答案

第1题: 除了变量语法不同,核心区别还有:

  • SCSS 用 Ruby/Dart 实现,LESS 用 JavaScript 实现
  • SCSS 有 @use/@forward 模块系统,LESS 只有 @import
  • SCSS 支持更复杂的逻辑(@if/@for/@each 等控制指令更强大)
  • SCSS 的 Mixin 支持参数默认值和可变参数,更灵活

第2题: 编译结果:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

Mixin 会把样式”复制”到引用的地方。

第3题: @mixin 适合需要传参的场景,每次 @include 都会生成一份独立的 CSS 代码。@extend 适合多个选择器共享同一组样式的场景,它通过合并选择器来减少重复代码。@mixin 在多次使用时会导致样式膨胀(代码重复),@extend 在复杂嵌套中可能生成意想不到的选择器组合。

CSS 预处理器解决了什么问题?

原生 CSS 有几个长期被诟病的痛点,预处理器就是为了解决它们而生的。

痛点一:没有变量

在原生 CSS 引入自定义属性(CSS Variables)之前,所有的颜色、尺寸都是硬编码的。想改一个主题色?得全局搜索替换。

// SCSS:定义变量,一处修改,处处生效
$primary-color: #1890ff;
$font-size-base: 14px;

.button {
  color: $primary-color;
  font-size: $font-size-base;
}

痛点二:选择器重复嵌套冗长

原生 CSS 中写嵌套样式,父选择器要重复写很多遍。

/* 原生 CSS:选择器重复 */
.nav { }
.nav .nav-item { }
.nav .nav-item .nav-link { }
.nav .nav-item .nav-link:hover { }
// SCSS:嵌套书写,结构清晰
.nav {
  .nav-item {
    .nav-link {
      &:hover { }
    }
  }
}

痛点三:没有代码复用机制

原生 CSS 没有函数、混入等机制,相似的样式只能复制粘贴。

// SCSS:Mixin 实现复用
@mixin ellipsis($lines: 1) {
  @if $lines == 1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

.title { @include ellipsis(1); }
.desc { @include ellipsis(3); }

痛点四:没有模块化

一个大项目的 CSS 可能有几万行,没有模块化机制很难维护。预处理器通过 @import(或 SCSS 的 @use)支持将样式拆分到多个文件中。

SCSS vs LESS:核心语法对比

变量

// SCSS 用 $
$color: #333;
$spacing: 16px;

.box {
  color: $color;
  padding: $spacing;
}
// LESS 用 @
@color: #333;
@spacing: 16px;

.box {
  color: @color;
  padding: @spacing;
}

重要区别:SCSS 的变量有作用域,LESS 的变量是”懒加载”的。

// SCSS:变量有块级作用域
$color: red;

.box {
  $color: blue; // 局部变量,不影响外部
  color: $color; // blue
}

.other {
  color: $color; // red
}
// LESS:变量是懒加载的,后面的定义会覆盖前面的
@color: red;

.box {
  color: @color; // blue(因为下面的定义覆盖了)
}

@color: blue;

嵌套

嵌套语法在 SCSS 和 LESS 中基本一致,& 代表父选择器。

// SCSS 和 LESS 写法相同
.card {
  border: 1px solid #eee;

  &-header {
    font-weight: bold;
  }

  &:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  }

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

编译结果:

.card { border: 1px solid #eee; }
.card-header { font-weight: bold; }
.card:hover { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
.card .title { font-size: 18px; }

Mixin

Mixin 是预处理器最强大的特性之一,相当于”样式函数”。

// SCSS Mixin
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'mobile' {
    @media (max-width: 767px) { @content; }
  } @else if $breakpoint == 'tablet' {
    @media (max-width: 1023px) { @content; }
  } @else if $breakpoint == 'desktop' {
    @media (min-width: 1024px) { @content; }
  }
}

.sidebar {
  width: 300px;

  @include respond-to('mobile') {
    width: 100%;
  }
}
// LESS Mixin
.respond-to(@breakpoint, @rules) {
  & when (@breakpoint = mobile) {
    @media (max-width: 767px) { @rules(); }
  }
  & when (@breakpoint = tablet) {
    @media (max-width: 1023px) { @rules(); }
  }
}

.sidebar {
  width: 300px;

  .respond-to(mobile, {
    width: 100%;
  });
}

可以看到,SCSS 的 Mixin 语法更直观,@content 关键字可以传递内容块,比 LESS 更灵活。

继承(@extend)

// SCSS @extend
%button-base {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary {
  @extend %button-base;
  background: #1890ff;
  color: #fff;
}

.btn-danger {
  @extend %button-base;
  background: #ff4d4f;
  color: #fff;
}

编译结果:

.btn-primary, .btn-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary {
  background: #1890ff;
  color: #fff;
}

.btn-danger {
  background: #ff4d4f;
  color: #fff;
}

%button-base 是 SCSS 的占位选择器,它不会被编译为实际的 CSS,只有被 @extend 引用时才会输出。

// LESS 的"继承"通过 Mixin 实现
.button-base() { // 加括号变成 Mixin,不会直接输出
  display: inline-flex;
  align-items: center;
  cursor: pointer;
}

.btn-primary {
  .button-base();
  background: #1890ff;
}

LESS 没有真正的 @extend,虽然后来加了 :extend() 语法,但用的人很少。

函数

// SCSS 内置函数
.box {
  color: darken(#1890ff, 10%);        // 加深颜色
  background: lighten(#1890ff, 30%);  // 减淡颜色
  border-color: rgba(#1890ff, 0.5);   // 添加透明度
}

// 自定义函数
@function px2rem($px) {
  @return $px / 75 * 1rem;
}

.title {
  font-size: px2rem(28); // 0.3733rem
}
// LESS 内置函数
.box {
  color: darken(#1890ff, 10%);
  background: lighten(#1890ff, 30%);
  border-color: fade(#1890ff, 50%);
}

// LESS 不支持自定义函数,但可以用变量和 Mixin 模拟

SCSS 支持自定义函数(@function),这是 LESS 所不具备的。

控制指令

// SCSS 的控制指令
@for $i from 1 through 5 {
  .col-#{$i} {
    width: 20% * $i;
  }
}

$themes: (
  'light': (#fff, #333),
  'dark': (#1a1a1a, #fff),
);

@each $name, $colors in $themes {
  .theme-#{$name} {
    background: nth($colors, 1);
    color: nth($colors, 2);
  }
}

LESS 也有循环,但语法不太直观:

// LESS 的循环
.generate-columns(@n, @i: 1) when (@i =< @n) {
  .col-@{i} {
    width: (100% / @n) * @i;
  }
  .generate-columns(@n, (@i + 1));
}

.generate-columns(5);

核心对比总结

特性SCSSLESS
变量语法$variable@variable
变量作用域块级作用域全局,懒加载
Mixin@mixin + @include直接用类名调用
继承@extend + 占位选择器:extend()(较少使用)
自定义函数@function不支持
控制指令@if/@for/@each/@whileGuards + 递归调用
模块系统@use/@forward@import
运行环境Dart(官方)/ NodeJavaScript(Node)

SCSS 的 @use/@forward vs LESS 的 @import

这是 SCSS 相比 LESS 最大的架构优势。

LESS 和旧版 SCSS 的 @import 问题

// _variables.scss
$primary: #1890ff;

// _mixins.scss
@import 'variables'; // 可以访问 $primary

// button.scss
@import 'variables';
@import 'mixins';

// card.scss
@import 'variables'; // 重复 import
@import 'mixins';    // 重复 import

@import 的问题:

  1. 所有变量都是全局的,容易命名冲突
  2. 重复加载:同一个文件被多次 @import,会被编译多次
  3. 没有封装:import 一个文件就等于把所有内容倒出来

SCSS 的 @use 模块系统

// _variables.scss
$primary: #1890ff;
$gap: 16px;

// button.scss
@use 'variables' as vars;

.button {
  color: vars.$primary;     // 通过命名空间访问
  padding: vars.$gap;
}

@use 的优点:

  1. 命名空间隔离:必须通过 namespace.$variable 访问,不会污染全局
  2. 只加载一次:无论被 @use 多少次,文件只编译一次
  3. 明确依赖关系:看 @use 就知道依赖了哪些模块

@forward 转发

@forward 用于”转发”一个模块的内容,常用于创建入口文件:

// _index.scss(入口文件)
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// 其他文件只需要 @use 入口文件
// button.scss
@use 'index' as *;

.button {
  color: $primary;
  @include flex-center;
}

LESS 的现状

LESS 目前只有 @import,没有类似 @use 的模块系统。对于大型项目来说,这是 LESS 的一个明显劣势。这也是为什么现在新项目更多选择 SCSS 的原因之一。

PostCSS 的定位与 Autoprefixer

PostCSS 不是预处理器

很多人把 PostCSS 和 SCSS/LESS 混为一谈,但它们的定位完全不同。

  • SCSS/LESS:CSS 预处理器,在写代码时使用特殊语法(变量、嵌套、Mixin),编译成标准 CSS
  • PostCSS:CSS 后处理器,对已经生成的标准 CSS 进行转换和优化

你可以理解为:SCSS 负责”写之前”,PostCSS 负责”写之后”。

PostCSS 的工作原理

PostCSS 本身只是一个 CSS 的解析器和生成器,它的能力完全来自插件

CSS 源码 → PostCSS 解析 → AST → 插件处理 → 生成新 CSS

常用 PostCSS 插件

Autoprefixer:自动添加浏览器前缀

/* 输入 */
.box {
  display: flex;
  user-select: none;
}

/* Autoprefixer 输出 */
.box {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

postcss-preset-env:让你使用未来的 CSS 语法

/* 输入:使用 CSS 嵌套(原生语法) */
.card {
  & .title {
    font-size: 18px;
  }
}

/* 输出:转换为兼容语法 */
.card .title {
  font-size: 18px;
}

postcss-pxtorem / postcss-px-to-viewport:px 转 rem/vw(移动端适配章节讲过)

SCSS + PostCSS 配合使用

在实际项目中,SCSS 和 PostCSS 通常是配合使用的:

SCSS 源码 → Sass 编译器 → 标准 CSS → PostCSS(Autoprefixer 等) → 最终 CSS
// vite.config.js 示例
export default {
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/variables" as *;`
      }
    },
    postcss: {
      plugins: [
        require('autoprefixer')
      ]
    }
  }
};

CSS Modules 和 CSS-in-JS

CSS Modules

CSS Modules 解决的是 CSS 的作用域问题。它不是一种新的语言,而是一种构建工具的能力。

/* Button.module.css */
.button {
  background: #1890ff;
  color: #fff;
  padding: 8px 16px;
}

.primary {
  background: #1890ff;
}
// Button.jsx
import styles from './Button.module.css';

function Button() {
  return <button className={styles.button}>点击</button>;
}

编译后的 HTML:

<button class="Button_button_1a2b3">点击</button>

类名被自动加上了唯一的哈希后缀,避免了全局命名冲突。

CSS Modules 和 SCSS 可以配合使用:

/* Button.module.scss */
@use '@/styles/variables' as *;

.button {
  background: $primary;

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

CSS-in-JS

CSS-in-JS 是把 CSS 直接写在 JavaScript 中的方案。常见的库有 styled-components、Emotion、Stitches 等。

// styled-components 示例
import styled from 'styled-components';

const Button = styled.button`
  background: ${props => props.primary ? '#1890ff' : '#fff'};
  color: ${props => props.primary ? '#fff' : '#333'};
  padding: 8px 16px;
  border: 1px solid #d9d9d9;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    opacity: 0.8;
  }
`;

// 使用
<Button primary>主要按钮</Button>

各方案对比

方案作用域隔离动态样式开发体验运行时开销
普通 CSS/SCSS不支持简单
CSS Modules有(编译时)不支持较好
CSS-in-JS有(运行时)支持
Tailwind CSS有(原子类)有限需要学习

当前的趋势:

  • React 生态:CSS Modules 或 Tailwind CSS 用的最多,styled-components 仍有大量项目在用
  • Vue 生态:Scoped CSS(<style scoped>)+ SCSS 是主流
  • 新兴方案:零运行时 CSS-in-JS(如 vanilla-extract、Panda CSS)正在崛起

常见误区

误区一:SCSS 和 Sass 是两个不同的东西

Sass 有两种语法:缩进语法.sass 文件)和 SCSS 语法.scss 文件)。SCSS 是 Sass 3.0 引入的新语法,完全兼容原生 CSS,所以现在大家说的”Sass”一般就是指”SCSS”。

// .sass 文件:缩进语法,没有花括号和分号
.button
  background: #1890ff
  color: #fff
  &:hover
    opacity: 0.8
// .scss 文件:SCSS 语法,更接近原生 CSS
.button {
  background: #1890ff;
  color: #fff;
  &:hover {
    opacity: 0.8;
  }
}

两种语法功能完全相同,只是书写格式不同。SCSS 因为兼容原生 CSS 语法,使用更广泛。

误区二:@extend 比 @mixin 好用,因为不会重复代码

@extend 确实不会像 @mixin 那样复制代码,但它有自己的问题:

// @extend 在嵌套中可能生成意想不到的选择器
.parent {
  .link {
    @extend %base-style;
  }
}

// 可能生成:.parent .link, .parent .other-link, ...
// 选择器组合可能非常复杂

最佳实践

  • 简单的样式共享用 @extend(配合占位选择器 %
  • 需要传参的复杂场景用 @mixin
  • @media 内不能使用 @extend

误区三:PostCSS 可以替代 SCSS

PostCSS 的插件(如 postcss-nested、postcss-simple-vars)确实可以实现类似 SCSS 的变量和嵌套功能,但如果你需要 Mixin、自定义函数、复杂的控制流等高级特性,PostCSS 就力不从心了。

在大多数项目中,SCSS 和 PostCSS 是互补关系,而不是替代关系。

误区四:CSS-in-JS 是最佳方案

CSS-in-JS 有运行时开销(需要在运行时解析和注入样式),在大型应用中可能影响性能。React 团队自己也在 React Server Components 的文档中提到了 CSS-in-JS 的局限性。

选择 CSS 方案要根据项目情况:

  • 小型项目:普通 CSS / SCSS 就够了
  • 组件库:CSS Modules 或 CSS-in-JS
  • 追求性能:CSS Modules + SCSS,或零运行时方案

小结

CSS 预处理器是前端工程化的重要组成部分。理解它们解决的问题和各自的特点,有助于你在面试中展现出工程化思维。

本章思维导图

CSS 预处理器与工程化
  • 预处理器解决的问题
    • 变量
    • 嵌套
    • 代码复用(Mixin)
    • 模块化
  • SCSS vs LESS
    • 变量:$ vs @
    • Mixin:@mixin/@include vs 类名调用
    • 继承:@extend vs :extend()
    • 函数:SCSS 支持自定义,LESS 不支持
    • 控制指令:SCSS 更强大
    • 模块系统:@use/@forward vs @import
  • PostCSS
    • 定位:后处理器,基于插件
    • Autoprefixer
    • postcss-preset-env
    • 与 SCSS 互补使用
  • 样式方案
    • CSS Modules(编译时作用域隔离)
    • CSS-in-JS(运行时,有性能开销)
    • Scoped CSS(Vue 特有)
    • Tailwind CSS(原子化 CSS)

练习挑战

挑战一:基础(⭐)

用 SCSS 写一个 Mixin,实现多行文本截断(参数为行数)。

点击查看答案
@mixin text-ellipsis($lines: 1) {
  overflow: hidden;

  @if $lines == 1 {
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    display: -webkit-box;
    -webkit-line-clamp: $lines;
    -webkit-box-orient: vertical;
    text-overflow: ellipsis;
  }
}

// 使用
.title {
  @include text-ellipsis(1); // 单行截断
}

.description {
  @include text-ellipsis(3); // 三行截断
}

编译结果:

.title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.description {
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  text-overflow: ellipsis;
}

挑战二:进阶(⭐⭐)

用 SCSS 的 @each 和 Map 批量生成以下 CSS:

.text-primary { color: #1890ff; }
.bg-primary { background-color: #1890ff; }
.text-success { color: #52c41a; }
.bg-success { background-color: #52c41a; }
.text-danger { color: #ff4d4f; }
.bg-danger { background-color: #ff4d4f; }
.text-warning { color: #faad14; }
.bg-warning { background-color: #faad14; }
点击查看答案
$colors: (
  'primary': #1890ff,
  'success': #52c41a,
  'danger':  #ff4d4f,
  'warning': #faad14,
);

@each $name, $color in $colors {
  .text-#{$name} {
    color: $color;
  }

  .bg-#{$name} {
    background-color: $color;
  }
}

这种模式在组件库和设计系统中非常常见,可以极大减少重复代码。如果要新增一个颜色,只需要在 $colors Map 中加一行就行了。

挑战三:综合(⭐⭐⭐)

设计一个 SCSS 项目的文件结构,要求:

  1. 使用 @use/@forward 模块系统
  2. 变量、Mixin、基础样式分离
  3. 每个组件有独立的样式文件
  4. 有一个统一的入口文件
点击查看答案
styles/
├── abstracts/          # 抽象层(不生成实际 CSS)
│   ├── _variables.scss  # 变量
│   ├── _mixins.scss     # Mixin
│   ├── _functions.scss  # 自定义函数
│   └── _index.scss      # 转发入口
├── base/               # 基础样式
│   ├── _reset.scss      # 重置样式
│   ├── _typography.scss # 排版
│   └── _index.scss      # 转发入口
├── components/         # 组件样式
│   ├── _button.scss
│   ├── _card.scss
│   ├── _modal.scss
│   └── _index.scss      # 转发入口
├── layouts/            # 布局样式
│   ├── _header.scss
│   ├── _sidebar.scss
│   └── _index.scss
└── main.scss           # 总入口

各文件内容:

// abstracts/_index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';

// base/_index.scss
@forward 'reset';
@forward 'typography';

// components/_button.scss
@use '../abstracts' as *;

.button {
  padding: $spacing-sm $spacing-md;
  border-radius: $radius;
  @include transition(all);
}

// main.scss(总入口)
@use 'base';
@use 'components';
@use 'layouts';

关键点:

  1. abstracts/ 里的文件只包含变量、Mixin、函数,不生成实际 CSS
  2. 每个目录有 _index.scss 作为转发入口,简化引用路径
  3. 组件样式通过 @use '../abstracts' as * 引入所需的工具
  4. main.scss 是总入口,按顺序引入各模块

自我检测

读完本章后,确认你能回答以下问题:

  • 能说出 CSS 预处理器解决的核心问题(至少 3 个)
  • 能说出 SCSS 和 LESS 在变量、Mixin、函数方面的语法区别
  • 知道 @mixin@extend 的区别和各自的适用场景
  • 能解释 SCSS 的 @use/@forward 相比 @import 的优势
  • 知道 PostCSS 的定位是”后处理器”,和 SCSS 是互补关系
  • 能说出 Autoprefixer 的作用
  • 知道 CSS Modules 如何实现作用域隔离
  • 能说出 CSS-in-JS 的优缺点
  • 知道 SCSS 和 Sass 的关系(同一个东西的两种语法)

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90