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);
核心对比总结
| 特性 | SCSS | LESS |
|---|---|---|
| 变量语法 | $variable | @variable |
| 变量作用域 | 块级作用域 | 全局,懒加载 |
| Mixin | @mixin + @include | 直接用类名调用 |
| 继承 | @extend + 占位选择器 | :extend()(较少使用) |
| 自定义函数 | @function | 不支持 |
| 控制指令 | @if/@for/@each/@while | Guards + 递归调用 |
| 模块系统 | @use/@forward | @import |
| 运行环境 | Dart(官方)/ Node | JavaScript(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 的问题:
- 所有变量都是全局的,容易命名冲突
- 重复加载:同一个文件被多次
@import,会被编译多次 - 没有封装:import 一个文件就等于把所有内容倒出来
SCSS 的 @use 模块系统
// _variables.scss
$primary: #1890ff;
$gap: 16px;
// button.scss
@use 'variables' as vars;
.button {
color: vars.$primary; // 通过命名空间访问
padding: vars.$gap;
}
@use 的优点:
- 命名空间隔离:必须通过
namespace.$variable访问,不会污染全局 - 只加载一次:无论被
@use多少次,文件只编译一次 - 明确依赖关系:看
@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 预处理器是前端工程化的重要组成部分。理解它们解决的问题和各自的特点,有助于你在面试中展现出工程化思维。
本章思维导图
- 预处理器解决的问题
- 变量
- 嵌套
- 代码复用(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 项目的文件结构,要求:
- 使用
@use/@forward模块系统 - 变量、Mixin、基础样式分离
- 每个组件有独立的样式文件
- 有一个统一的入口文件
点击查看答案
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';
关键点:
abstracts/里的文件只包含变量、Mixin、函数,不生成实际 CSS- 每个目录有
_index.scss作为转发入口,简化引用路径 - 组件样式通过
@use '../abstracts' as *引入所需的工具 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