第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\n
\n\n
\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-sass 或 sass 时开启 --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 项目「活」起来。

评论(0)