第5章 5.3 CSS 预处理器:Sass / Less

🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 Vant 快速搭出一个好看的移动端页面,但有个问题不知道你发现没有——写样式太累了

比如你要给按钮设置颜色,得写 .btn { color: #333; },然后子元素 .btn .icon { margin-right: 5px; },再然后 .btn .icon .svg { width: 16px; }……一层套一层,括号嵌套到眼瞎

而且项目做到一半,产品说「把这个蓝色统一改成品牌紫」,你得 ctrl+F 搜索整个文件,一个个改,改完还怕漏掉某个地方。

这就是为什么我们需要 CSS 预处理器——它让 CSS 学会「编程」,能声明变量、能嵌套、能复用。用 Sass/Less 写样式,就像从「手写汇编」升级到「写 Python」。

学完这章,你就能:
- 用变量管理一套主题色,改一处全局生效
- 用嵌套告别 .parent .child .grandson 的噩梦
- 用 mixin 复\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n用样式块,像搭积木一样拼页面


🧱 基础 25 分钟:核心概念

什么是 Sass / Less?

生活类比:普通 CSS 像是「做菜只给原料清单」,而 Sass/Less 像是「给你一个半自动厨房」——你能定义配方(变量)、预设烹饪模式(mixin),做出来的菜还能存档下次直接用。

Sass 和 Less 本质上都是「CSS 的超集」,它们自己的语法最终会编译成普通 CSS 浏览器才能认。

Sass Less
语法风格 用缩进表示层级 用花括号表示层级
变量符号 $变量名 @变量名
社区热度 更流行,Vue 官方推荐 同样流行,Bootstrap 在用

Vue 项目里怎么选? Vue 官方 CLI 内置支持 Sass,所以无脑选 Sass 就对了

1. 变量:像给颜色起个名字

痛点:你写了个 #ff6b6b 红色,用了 20 个地方,产品说要换成 #e74c3c,你得改 20 处。

生活类比:变量就像是给电话号码存联系人——你不用记 138xxxx,你记「老妈的号码」,改手机号只改联系人就行。

// Sass 语法(.scss 文件)
// 定义变量
$brand-color: #ff6b6b;
$secondary-color: #4ecdc4;
$spacing-unit: 16px;

// 使用变量
.header {
color: $brand-color;
padding: $spacing-unit;
}

.button {
background: $brand-color;
border: 1px solid $secondary-color;
}

编译成普通 CSS:

.header {
color: #ff6b6b;
padding: 16px;
}
.button {
background: #ff6b6b;
border: 1px solid #4ecdc4;
}

变量命名建议用 $primary-color$font-size-lg 这种语义化名字,别用 $c1$var1,不然过两周自己都看不懂。

2. 嵌套:告别重复写父级选择器

痛点:HTML 结构是 .container > .nav > .item > .link,CSS 得写 .container .nav .item .link {},层叠选择器写到吐。

生活类比:嵌套就像写家谱——「老王家的小王的媳妇」不用每次都说「老王家」,直接 老王 { 小王 { 媳妇 { ... } } }

// 原始写法:重复写父级
.nav {
background: #333;
}
.nav .item {
color: #fff;
}
.nav .item .link {
text-decoration: none;
}

// Sass 嵌套写法
.nav {
background: #333;

.item {
color: #fff;

.link {
  text-decoration: none;

  &:hover {
    color: $brand-color;
  }
}
}
}

& 符号表示「父选择器」,:hover 可以直接接在 .link 后面,不用再写 .nav .item .link:hover

3. Mixin:样式复用,像函数一样

痛点:项目中很多按钮长得差不多,只是颜色不同,你不想复制粘贴 10 份。

生活类比:Mixin 就像「模具」——你要做 100 个月饼,不用一个个捏,买个月饼模具,往里面倒馅就行了。

// 定义 mixin(参数可以带默认值)
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}

@mixin button-style($bg-color: #333, $text-color: #fff) {
background: $bg-color;
color: $text-color;
padding: 10px 20px;
border-radius: 4px;
border: none;
cursor: pointer;

&:hover {
opacity: 0.8;
}
}

// 使用 mixin
.primary-btn {
@include button-style($brand-color, #fff);
}

.secondary-btn {
@include button-style(#fff, #333);
}

编译后:

.primary-btn {
background: #ff6b6b;
color: #fff;
padding: 10px 20px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.primary-btn:hover {
opacity: 0.8;
}
.secondary-btn {
background: #fff;
color: #333;
padding: 10px 20px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.secondary-btn:hover {
opacity: 0.8;
}

4. Vue 项目中使用 Sass

Vue 组件的 <style> 标签加上 lang="scss" 就能用 Sass 语法了:

<template>
<div class="card">
<h2 class="card-title">Sass 真香</h2>
<button class="btn-primary">点我</button>
</div>
</template>

<style lang="scss">
// 定义变量
$card-bg: #f8f9fa;
$accent: #6c5ce7;

.card {
background: $card-bg;
padding: 20px;
border-radius: 8px;

&-title {
color: $accent;
margin-bottom: 16px;
}

.btn-primary {
@include button-style($accent);
}
}
</style>

注意:如果报错 Syntax Error: TypeError: this.getOptions is not a function,记得先装依赖 npm install -D sass sass-loader

5. scoped CSS + Sass 一起用

Vue 的 scoped 让样式只作用于当前组件,但有时候你需要在子组件根元素上加点样式:

<style lang="scss" scoped>
.card {
// 这里的样式只影响 .card,不会影响子组件

// :deep() 可以穿透 scoped,作用于子组件内部
:deep(.el-button) {
margin-top: 10px;
}
}
</style>

🔥 实战 35 分钟:3 个递进小项目

项目 1(5 分钟):用 Sass 变量做主题切换

目标:定义一套颜色变量,模拟换肤功能。

<template>
<div class="theme-demo">
<h1>🎨 Sass 变量演示</h1>

<div class="btn-group">
  <button 
    v-for="theme in themes" 
    :key="theme.name"
    :class="['theme-btn', theme.name]"
    @click="currentTheme = theme.name"
  >
    {{ theme.label }}
  </button>
</div>

<div class="card-container">
  <div class="card">
    <h2>卡片标题</h2>
    <p>这是一段示例文字,用来展示 Sass 变量的强大之处。</p>
    <button class="primary-btn">主要按钮</button>
  </div>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue'

const themes = [
{ name: 'default', label: '默认主题' },
{ name: 'dark', label: '暗黑主题' },
{ name: 'ocean', label: '海洋主题' }
]

const currentTheme = ref('default')
</script>

<style lang="scss" scoped>
// Sass 变量定义
$default-primary: #ff6b6b;
$default-secondary: #4ecdc4;
$default-bg: #ffffff;

$dark-primary: #a29bfe;
$dark-secondary: #6c5ce7;
$dark-bg: #2d3436;

$ocean-primary: #0984e3;
$ocean-secondary: #00cec9;
$ocean-bg: #dfe6e9;

// 根据主题选择颜色
$primary-color: if($default-primary, $default-primary, $default-primary);

.theme-demo {
padding: 20px;
font-family: Arial, sans-serif;

h1 {
margin-bottom: 20px;
}
}

.btn-group {
margin-bottom: 20px;

.theme-btn {
padding: 8px 16px;
margin-right: 10px;
border: 2px solid transparent;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;

&.default { background: $default-primary; color: #fff; }
&.dark { background: $dark-primary; color: #fff; }
&.ocean { background: $ocean-primary; color: #fff; }

&:hover {
  transform: scale(1.05);
}
}
}

.card-container {
.card {
background: $default-bg;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);

h2 {
  color: $default-primary;
  margin-bottom: 12px;
}

p {
  color: #666;
  line-height: 1.6;
  margin-bottom: 16px;
}
}
}

.primary-btn {
background: $default-primary;
color: #fff;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;

&:hover {
opacity: 0.85;
}
}
</style>

预期输出:点击不同主题按钮,卡片颜色会切换。

一句话解释:通过 Sass 变量集中管理颜色,改一处全局生效。


项目 2(15 分钟):用 Mixin 批量生成按钮组件

目标:用 mixin 快速生成多种样式的按钮,减少重复代码。

<template>
<div class="button-demo">
<h1>🔥 Mixin 批量生成按钮</h1>
<p class="subtitle">同一个模具,不同颜色</p>

<div class="button-grid">
  <button class="btn btn-primary">主要操作</button>
  <button class="btn btn-success">成功</button>
  <button class="btn btn-warning">警告</button>
  <button class="btn btn-danger">危险</button>
  <button class="btn btn-outline">描边按钮</button>
  <button class="btn btn-large">大号按钮</button>
  <button class="btn btn-small">小号按钮</button>
  <button class="btn btn-disabled">禁用状态</button>
</div>
</div>
</template>

<script setup>
// 逻辑为空,纯粹展示样式
</script>

<style lang="scss" scoped>
// ==================== 变量定义 ====================
$btn-primary: #ff6b6b;
$btn-success: #26de81;
$btn-warning: #fed330;
$btn-danger: #fc5c65;
$btn-outline: #74b9ff;
$btn-disabled: #a4b0be;

$font-stack: 'Helvetica Neue', Arial, sans-serif;

// ==================== Mixin 定义 ====================

// 基础按钮样式
@mixin btn-base {
display: inline-flex;
align-items: center;
justify-content: center;
font-family: $font-stack;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;

&:active {
transform: scale(0.96);
}
}

// 颜色主题
@mixin btn-color($bg, $color: #fff) {
background: $bg;
color: $color;

&:hover {
filter: brightness(1.1);
box-shadow: 0 4px 12px rgba($bg, 0.4);
}
}

// 尺寸
@mixin btn-size($padding-v, $padding-h, $font-size) {
padding: $padding-v $padding-h;
font-size: $font-size;
}

// 描边样式
@mixin btn-outline($border-color) {
background: transparent;
color: $border-color;
border: 2px solid $border-color;

&:hover {
background: $border-color;
color: #fff;
}
}

// ==================== 按钮实现 ====================

.button-demo {
padding: 30px;

h1 {
margin-bottom: 8px;
}

.subtitle {
color: #666;
margin-bottom: 24px;
}
}

.button-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
}

.btn {
@include btn-base;
@include btn-size(12px, 24px, 14px);

// 各种颜色变体
&-primary { @include btn-color($btn-primary); }
&-success { @include btn-color($btn-success); }
&-warning { @include btn-color($btn-warning, #333); }
&-danger { @include btn-color($btn-danger); }

// 描边变体
&-outline {
@include btn-outline($btn-outline);
}

// 尺寸变体
&-large {
@include btn-size(16px, 32px, 18px);
}

&-small {
@include btn-size(6px, 12px, 12px);
}

// 禁用状态
&-disabled {
@include btn-color($btn-disabled);
cursor: not-allowed;
opacity: 0.6;

&:hover {
  filter: none;
  box-shadow: none;
}
}
}
</style>

预期输出:页面上显示 8 个不同样式(颜色、大小、描边、禁用)的按钮。

一句话解释:Mixin 就像「函数」,把公共样式抽出来,不同参数生成不同效果。


项目 3(15 分钟):Sass 实战——做一个待办清单样式系统

目标:用 Sass 变量 + 嵌套 + mixin 做一个待办清单的完整样式系统。

<template>
<div class="todo-app">
<header class="todo-header">
  <h1>📝 待办清单</h1>
  <p class="todo-subtitle">用 Sass 样式系统做的哦</p>
</header>

<div class="input-section">
  <input 
    v-model="newTask" 
    class="task-input"
    placeholder="输入新任务..."
    @keyup.enter="addTask"
  />
  <button class="add-btn" @click="addTask">添加</button>
</div>

<ul class="todo-list">
  <li 
    v-for="(task, index) in tasks" 
    :key="index"
    :class="['todo-item', { 'todo-item--done': task.done }]"
  >
    <span class="checkbox" @click="toggleTask(index)">
      {{ task.done ? '✅' : '⬜' }}
    </span>
    <span class="task-text">{{ task.text }}</span>
    <button class="delete-btn" @click="deleteTask(index)">×</button>
  </li>
</ul>

<footer class="todo-footer">
  共 {{ tasks.length }} 项任务,已完成 {{ doneCount }} 项
</footer>
</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const newTask = ref('')
const tasks = ref([
{ text: '学习 Sass 变量', done: true },
{ text: '掌握 Mixin 用法', done: false },
{ text: '用嵌套写样式', done: false }
])

const doneCount = computed(() => tasks.value.filter(t => t.done).length)

const addTask = () => {
if (newTask.value.trim()) {
tasks.value.push({ text: newTask.value, done: false })
newTask.value = ''
}
}

const toggleTask = (index) => {
tasks.value[index].done = !tasks.value[index].done
}

const deleteTask = (index) => {
tasks.value.splice(index, 1)
}
</script>

<style lang="scss" scoped>
// ==================== 主题变量 ====================
$theme-primary: #6c5ce7;
$theme-success: #00b894;
$theme-danger: #ff7675;
$theme-bg: #dfe6e9;
$theme-card: #ffffff;

$spacing-xs: 8px;
$spacing-sm: 12px;
$spacing-md: 16px;
$spacing-lg: 24px;

$radius-sm: 6px;
$radius-md: 10px;

$shadow-sm: 0 2px 8px rgba(0,0,0,0.08);
$shadow-md: 0 4px 16px rgba(0,0,0,0.12);

// ==================== Mixin ====================
@mixin flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}

@mixin card-style {
background: $theme-card;
border-radius: $radius-md;
box-shadow: $shadow-sm;
}

// ==================== 主容器 ====================
.todo-app {
max-width: 480px;
margin: 40px auto;
padding: $spacing-lg;
background: linear-gradient(135deg, #a29bfe 0%, #6c5ce7 100%);
border-radius: $radius-md;
min-height: 500px;
}

.todo-header {
text-align: center;
margin-bottom: $spacing-lg;

h1 {
color: #fff;
font-size: 28px;
margin-bottom: $spacing-xs;
}

.todo-subtitle {
color: rgba(255,255,255,0.8);
font-size: 14px;
}
}

// ==================== 输入区域 ====================
.input-section {
@include flex-between;
margin-bottom: $spacing-lg;
gap: $spacing-sm;

.task-input {
flex: 1;
padding: $spacing-sm $spacing-md;
border: 2px solid transparent;
border-radius: $radius-sm;
font-size: 16px;
outline: none;
transition: border-color 0.2s;

&:focus {
  border-color: rgba(255,255,255,0.5);
}
}

.add-btn {
padding: $spacing-sm $spacing-md;
background: $theme-success;
color: #fff;
border: none;
border-radius: $radius-sm;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s;

&:hover {
  transform: scale(1.05);
}
}
}

// ==================== 列表 ====================
.todo-list {
list-style: none;
padding: 0;
margin: 0 0 $spacing-lg 0;
}

.todo-item {
@include flex-between;
@include card-style;
padding: $spacing-sm $spacing-md;
margin-bottom: $spacing-sm;
transition: all 0.2s;

&:hover {
box-shadow: $shadow-md;
transform: translateX(4px);
}

// 完成状态的样式
&--done {
opacity: 0.6;

.task-text {
  text-decoration: line-through;
  color: #999;
}
}

.checkbox {
font-size: 20px;
cursor: pointer;
margin-right: $spacing-sm;
}

.task-text {
flex: 1;
color: #333;
}

.delete-btn {
width: 28px;
height: 28px;
background: $theme-danger;
color: #fff;
border: none;
border-radius: 50%;
font-size: 18px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s;
}

&:hover .delete-btn {
opacity: 1;
}
}

// ==================== 页脚 ====================
.todo-footer {
text-align: center;
color: rgba(255,255,255,0.9);
font-size: 14px;
}
</style>

预期输出:一个带渐变背景的待办清单,可以添加、完成、删除任务。

一句话解释:用 Sass 的变量管理主题色、用 mixin 复用布局样式,代码量少一半,可维护性翻倍。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:变量名冲突——全局污染

// ❌ 错误:两个文件都定义了 $primary-color,容易冲突
// _header.scss
$primary-color: #ff6b6b;

// _button.scss
$primary-color: #4ecdc4;

// ✅ 正确:用命名空间区分
$header-primary: #ff6b6b;
$button-primary: #4ecdc4;

坑 2:嵌套过深——选择器特异性爆炸

// ❌ 错误:嵌套 6 层,编译后选择器又长又难覆盖
.header {
.nav {
.list {
  .item {
    .link {
      .icon {
        width: 16px;
      }
    }
  }
}
}
}

// ✅ 正确:最多嵌套 3-4 层,考虑拆分成组件
.header {
.nav {
.item .link .icon {  // 或者用 BEM 命名
  width: 16px;
}
}
}

坑 3:Mixin 忘记加括号调用

// ❌ 错误:忘了 @include,只写了 mixin 名
.my-btn {
button-style;  // 编译后什么都不会发生
}

// ✅ 正确:记得加 @include
.my-btn {
@include button-style;
}

坑 4:编译报错 "Undefined variable"

// ❌ 错误:使用了没定义的变量
.element {
color: $undefined-color;
}

// ✅ 正确:确保变量在使用前已定义,或检查 import 顺序
$brand-color: #ff6b6b;

坑 5:Scss 和 Sass 语法混用

// ❌ 错误:混用两种语法
$var: 16px;  // scss 语法
%placeholder  // sass 语法(没有等号)
color: red

性能小贴士:按需编译

如果项目很大,用 node-sasssass 时开启 --watch 模式,只编译改动的文件:

# 监听单个文件
sass --watch src/styles/main.scss dist/styles/main.css

# 监听整个目录
sass --watch src/styles:dist/styles

调试技巧:用 SassMeister 预览

写完 Sass 不确定编译结果?去 SassMeister 在线测试,实时看编译后的 CSS。


✏️ 练习题

练习 1(2 分钟):改颜色变量

  • 输入:把项目 1 的 $default-primary#ff6b6b 改成 #e056fd
  • 预期输出:所有用到这个变量的地方都变成紫色

练习 2(2 分钟):加一个条件判断

  • 输入:在项目 2 的 .btn-disabled 上加一行判断,禁用时鼠标变成 not-allowed
  • 预期输出:鼠标悬停在禁用按钮上显示禁止图标

练习 3(3 分钟):写一个新的 Mixin

  • 输入:写一个 @mixin text-ellipsis 让文字超长时显示省略号
  • 预期输出overflow: hidden; text-overflow: ellipsis; white-space: nowrap;

练习 4(5 分钟):组合主题

  • 输入:给项目 3 的待办清单加一个「完成时背景变绿」的效果,用嵌套语法
  • 预期输出:任务完成后整行背景变浅绿色

练习 5(3 分钟):分析报错

  • 输入:代码中写了 color: $primary-color,但页面报错 Undefined variable
  • 预期输出:说出 3 种可能的原因

作业:做一个「主题切换器」

需求描述:做一个支持切换 3 套主题的卡片组件,点击不同主题按钮,整站颜色随之切换。

功能点
1. 定义 3 套主题变量(默认、暗色、渐变)
2. 点击按钮切换主题,样式实时更新
3. 用 Mixin 统一按钮和卡片的样式

加分项
1. 主题切换有过渡动画
2. 刷新页面保持上次选择的主题(localStorage)

验收标准
- 能跑起来,点击按钮主题切换
- 变量用 Sass 管理,不是硬编码颜色值
- 代码有适当注释


📚 总结 + 资源

一句话总结:Sass 让 CSS 学会「编程」——变量管颜色、嵌套少写选择器、Mixin 复用样式块。

延伸资源
- Sass 官方文档(最权威,有中文版)
- Vue 3 + Vite + Sass 项目模板(可以直接抄)
- B 站「Sass 入门到精通」系列视频(适合视觉学习者)


互动钩子:你在上一个项目里有没有遇到「改颜色改到崩溃」的情况?用什么方法解决的?评论区聊聊,老粉优先回复!


下一章我们要解决一个问题:做好的页面很「死」,点击按钮没反应、切换页面很生硬——下一章我们学习 动画与过渡,让你的 Vue 项目「活」起来。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。