Vue3 中 SCSS/Less 快捷写法 —— 嵌套、Mixin、循环与工程化实践

Vue3 中 SCSS/Less 快捷写法

Vue3 单文件组件(SFC)天然支持预处理器,但在日常开发中很多写法都停留在原生 CSS 水平。本文整理一套 SCSS/Less 的快捷写法,涵盖嵌套、变量、Mixin、循环、函数等核心特性,写完就能提效 50% 以上。

一、Vue3 样式块基础配置

1.1 <style> 标签的四种形态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 1. scoped:样式隔离 -->
<style scoped lang="scss">
.box { ... }
</style>

<!-- 2. module:CSS Modules -->
<style module lang="scss">
.active {
color: red;
}
</style>
<!-- 使用时:<div :class="$style.active"> -->

<!-- 3. v-bind:JS 变量驱动 CSS -->
<script setup>
const primaryColor = ref("#1890ff");
</script>
<style scoped>
.header {
color: v-bind(primaryColor);
}
</style>

<!-- 4. 多块并行 -->
<style scoped lang="scss">
/* 组件样式 */
</style>
<style lang="scss">
/* 全局样式 */
</style>

1.2 Vite 全局注入变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vite.config.ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// 每个 .vue 文件自动注入
additionalData: `
@use "@/styles/variables.scss" as *;
@use "@/styles/mixins.scss" as *;
`,
},
},
},
});

Less 同理:

1
2
3
4
5
6
7
8
css: {
preprocessorOptions: {
less: {
additionalData: `@import "@/styles/variables.less";`,
javascriptEnabled: true
}
}
}
配置项SCSSLess
路径别名~@/@/~@/@/
全局注入additionalDataadditionalData
数学运算默认 / 是除法javascriptEnabled: true

二、嵌套语法 —— 告别重复选择器

2.1 基础嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.card {
padding: 16px;
border-radius: 8px;
background: #fff;

// 子元素
.title {
font-size: 18px;
font-weight: 600;
}

// 伪类
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

// 伪元素
&::before {
content: "";
display: block;
}

// 属性选择器
&[data-type="primary"] {
background: var(--primary);
}

// 兄弟选择器
& + .card {
margin-top: 12px;
}
}

2.2 BEM 命名快捷写法

1
2
3
4
5
6
7
8
9
10
11
12
13
.block {
// &__element
&__header { ... }
&__body { ... }
&__footer { ... }

// &--modifier
&--active { ... }
&--disabled {
opacity: 0.5;
pointer-events: none;
}
}

编译结果:

1
2
3
4
.block__header { ... }
.block__body { ... }
.block--active { ... }
.block--disabled { opacity: 0.5; pointer-events: none; }

2.3 媒体查询内嵌

1
2
3
4
5
6
7
8
9
10
11
12
.sidebar {
width: 250px;

@media (max-width: 768px) {
width: 100%;
display: none;
}

@media (min-width: 1200px) {
width: 300px;
}
}

Less 写法一致,但变量插值语法不同:

1
2
3
4
5
6
7
8
9
// Less 中
@width: 200px;
.sidebar {
width: @width;

@media (max-width: (@width * 2)) {
width: 100%;
}
}

三、变量系统 —— 一处定义,全局生效

3.1 基础变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// variables.scss
// 颜色
$primary: #1890ff;
$success: #52c41a;
$warning: #faad14;
$danger: #f5222d;

// 尺寸
$header-height: 56px;
$sidebar-width: 240px;
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 16px;

// 字体
$font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
$font-size-base: 14px;
$font-size-lg: 16px;
$font-size-sm: 12px;

// 间距
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;

SCSS vs Less 变量语法差异

特性SCSSLess
定义$name: value@name: value
引用$name@name
插值#{$name}@{name}
作用域块级 + 全局 !global惰性求值,后定义覆盖前
计算$a / $b@a / @b

3.2 CSS 自定义属性联动

1
2
3
4
5
6
7
8
9
10
11
12
13
// 把 SCSS 变量转为 CSS 变量,支持运行时修改
:root {
--primary: #{$primary};
--header-height: #{$header-height};
--radius-md: #{$radius-md};
}

// 使用
.button {
background: var(--primary);
height: var(--header-height);
border-radius: var(--radius-md);
}

3.3 Map 变量(SCSS 独有)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 主题色集合
$theme-colors: (
"primary": #1890ff,
"success": #52c41a,
"warning": #faad14,
"danger": #f5222d,
"info": #909399,
);

// 取值
.tag {
color: map-get($theme-colors, "primary");
}

四、Mixin —— 重复代码的终结者

4.1 弹性布局一行搞定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mixins.scss
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $gap: 0) {
display: flex;
flex-direction: $direction;
justify-content: $justify;
align-items: $align;
gap: $gap;
}

// 使用
.header {
@include flex(row, space-between, center); // 水平两端对齐居中
}
.column {
@include flex(column, center, center, $gap: 12px); // 垂直居中 + 间距
}

4.2 文本省略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@mixin text-ellipsis($line: 1) {
overflow: hidden;
text-overflow: ellipsis;

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

// 使用
.title {
@include text-ellipsis;
} // 单行
.desc {
@include text-ellipsis(3);
} // 三行

4.3 文本样式快捷

1
2
3
4
5
6
@mixin text-style($size: $font-size-base, $weight: normal, $color: inherit, $line-height: 1.5) {
font-size: $size;
font-weight: $weight;
color: $color;
line-height: $line-height;
}

4.4 居中方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@mixin center($type: "both") {
@if $type == "both" {
display: flex;
justify-content: center;
align-items: center;
} @else if $type == "horizontal" {
display: flex;
justify-content: center;
} @else if $type == "vertical" {
display: flex;
align-items: center;
} @else if $type == "absolute" {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}

4.5 响应式断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 断点定义
$breakpoints: (
"sm": 640px,
"md": 768px,
"lg": 1024px,
"xl": 1280px,
"2xl": 1536px,
);

@mixin respond($breakpoint, $type: "down") {
$width: map-get($breakpoints, $breakpoint);

@if $type == "down" {
@media (max-width: $width) {
@content;
}
} @else if $type == "up" {
@media (min-width: $width + 1px) {
@content;
}
} @else if $type == "only" {
$next: map-get($breakpoints, $breakpoint); // 简化示例
@media (min-width: $width + 1px) and (max-width: 1200px) {
@content;
}
}
}

// 使用
.card {
width: 33.33%;

@include respond(md) {
width: 50%;
}
@include respond(sm) {
width: 100%;
}
}

4.6 Less Mixin 对比

1
2
3
4
5
6
7
8
9
10
11
12
// Less 中 Mixin 不需要 @mixin 关键字
.flex(@direction: row, @justify: flex-start, @align: stretch) {
display: flex;
flex-direction: @direction;
justify-content: @justify;
align-items: @align;
}

// 使用:不加 @include,直接调用
.header {
.flex(row, space-between, center);
}
特性SCSS MixinLess Mixin
定义@mixin name($param) { }.name(@param) { }
调用@include name(val).name(val);
默认参数$param: default@param: default
可变参数$params...@params...
条件判断@if / @elsewhen 守卫

五、循环生成 —— 批量创建工具类

5.1 间距工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
$spacing-map: (
0: 0,
1: 8px,
2: 16px,
3: 24px,
4: 32px,
5: 48px,
);

// 生成 .m-0 ~ .m-5, .p-0 ~ .p-5, .gap-0 ~ .gap-5
@each $key, $val in $spacing-map {
.m-#{$key} {
margin: $val;
}
.mt-#{$key} {
margin-top: $val;
}
.mb-#{$key} {
margin-bottom: $val;
}
.ml-#{$key} {
margin-left: $val;
}
.mr-#{$key} {
margin-right: $val;
}
.mx-#{$key} {
margin-left: $val;
margin-right: $val;
}
.my-#{$key} {
margin-top: $val;
margin-bottom: $val;
}

.p-#{$key} {
padding: $val;
}
.pt-#{$key} {
padding-top: $val;
}
.pb-#{$key} {
padding-bottom: $val;
}
.pl-#{$key} {
padding-left: $val;
}
.pr-#{$key} {
padding-right: $val;
}
.px-#{$key} {
padding-left: $val;
padding-right: $val;
}
.py-#{$key} {
padding-top: $val;
padding-bottom: $val;
}

.gap-#{$key} {
gap: $val;
}
}

5.2 字体大小工具类

1
2
3
4
5
6
7
$font-sizes: 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48;

@each $size in $font-sizes {
.text-#{$size} {
font-size: #{$size}px;
}
}

5.3 主题色变体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$theme-colors: (
"primary": #1890ff,
"success": #52c41a,
"warning": #faad14,
"danger": #f5222d,
);

// 生成 .bg-primary, .text-primary, .border-primary 等
@each $name, $color in $theme-colors {
.bg-#{$name} {
background-color: $color;
}
.text-#{$name} {
color: $color;
}
.border-#{$name} {
border-color: $color;
}
}

// 生成颜色变浅 10% 的 hover 态
@each $name, $color in $theme-colors {
.btn-#{$name} {
background: $color;
&:hover {
background: lighten($color, 10%);
}
&:active {
background: darken($color, 10%);
}
}
}

5.4 透明度工具类

1
2
3
4
5
6
@for $i from 1 through 9 {
.opacity-#{$i}0 {
opacity: $i * 0.1;
}
}
// 生成 .opacity-10 ~ .opacity-90

5.5 Less 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Less 使用递归实现循环
@spacing: 0, 8, 16, 24, 32, 48;

.generator(@i: 1) when (@i <= length(@spacing)) {
@val: extract(@spacing, @i);
.m-@{i} {
margin: unit(@val, px);
}
.p-@{i} {
padding: unit(@val, px);
}
.generator(@i + 1);
}
.generator();

六、函数 —— 数值计算复用

6.1 像素转 rem/vw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 基准 16px = 1rem
@function rem($px) {
@return calc($px / 16) * 1rem;
}

// 设计稿宽 375px 下的 vw 转换
@function vw($px) {
@return calc($px / 375) * 100vw;
}

// 使用
.title {
font-size: rem(24); // 1.5rem
padding: vw(16); // 4.27vw
}

6.2 颜色透明度

1
2
3
4
5
6
7
8
// 给 hex 颜色加透明度,输出 rgba
@function alpha($color, $opacity) {
@return rgba($color, $opacity);
}

.overlay {
background: alpha(#1890ff, 0.15); // rgba(24, 144, 255, 0.15)
}

6.3 数学辅助

1
2
3
4
5
6
7
8
9
@function half($value) {
@return $value / 2;
}
@function double($value) {
@return $value * 2;
}
@function neg($value) {
@return -$value;
}

6.4 Less 函数

1
2
3
.rem(@px) {
return: unit(@px / 16, rem);
}

七、@extend —— 选择器继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义占位符(不生成 CSS)
%card-base {
padding: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

// 三个不同卡片继承同一套基础样式
.user-card {
@extend %card-base;
border-left: 4px solid $primary;
}
.article-card {
@extend %card-base;
border-left: 4px solid $success;
}
.notice-card {
@extend %card-base;
border-left: 4px solid $warning;
}

编译结果(选择器分组):

1
2
3
4
5
6
7
8
.user-card,
.article-card,
.notice-card {
padding: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

@extend vs @mixin 选择指南

维度@extend@mixin
CSS 输出分组选择器每次 include 复制一份
文件体积可能大(重复)
参数支持支持
适合场景样式完全相同需要参数变化

简单规则:需要传参时用 @mixin,不需要时用 %placeholder + @extend

八、Less 独有特性速览

特性语法说明
变量@name: value可被后续覆盖(惰性求值)
Mixin.name() { }无需 @mixin 关键字
调用.name();无需 @include
守卫.name() when (@width > 100)条件判断
运算@a + @b自动处理单位
合并属性background+: url(a.png)逗号合并
父选择器&和 SCSS 一样
循环递归 Mixin@for/@each,需手动递归

九、SCSS 独有特性速览

特性语法说明
变量$name: value块级作用域,不会被覆盖
Mapmap-get($map, key)键值对结构
Listnth($list, 1)列表操作
@function@function name($p) { @return $p }自定义函数
@if / @else原生条件判断比 Less 的 when 更直观
@for@for $i from 1 through 10数值循环
@each@each $item in $list列表遍历
@while@while $i > 0条件循环
@extend%placeholder + @extend选择器继承
内置函数lighten() darken() mix()颜色计算等

十、实战模板 —— 三文件结构

styles/variables.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// === 颜色 ===
$primary: #1890ff;
$success: #52c41a;
$warning: #faad14;
$danger: #f5222d;
$text-primary: #303133;
$text-regular: #606266;
$text-secondary: #909399;
$border-color: #dcdfe6;
$bg-color: #f5f7fa;

// === 尺寸 ===
$header-height: 56px;
$sidebar-width: 240px;
$radius-sm: 4px;
$radius-md: 8px;
$radius-lg: 16px;

// === 字体 ===
$font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
$font-size-base: 14px;
$font-size-sm: 12px;
$font-size-lg: 16px;

// === 间距 ===
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;

// === 阴影 ===
$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
$shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);

// === 断点 ===
$breakpoints: (
"sm": 640px,
"md": 768px,
"lg": 1024px,
"xl": 1280px,
);

// === z-index ===
$z-dropdown: 1000;
$z-modal: 2000;
$z-toast: 3000;

styles/mixins.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 弹性布局
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $gap: 0) {
display: flex;
flex-direction: $direction;
justify-content: $justify;
align-items: $align;
gap: $gap;
}

// 文本省略
@mixin text-ellipsis($line: 1) {
overflow: hidden;
text-overflow: ellipsis;
@if $line == 1 {
white-space: nowrap;
} @else {
display: -webkit-box;
-webkit-line-clamp: $line;
-webkit-box-orient: vertical;
}
}

// 绝对居中
@mixin abs-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

// 清除浮动
@mixin clearfix {
&::after {
content: "";
display: block;
clear: both;
}
}

// 滚动条美化
@mixin scrollbar($width: 6px, $thumb: #c0c4cc, $track: transparent) {
&::-webkit-scrollbar {
width: $width;
height: $width;
}
&::-webkit-scrollbar-thumb {
background: $thumb;
border-radius: $width;
}
&::-webkit-scrollbar-track {
background: $track;
}
}

// 响应式
@mixin respond($bp, $type: "down") {
$width: map-get($breakpoints, $bp);
@if $type == "down" {
@media (max-width: $width) {
@content;
}
} @else {
@media (min-width: $width + 1px) {
@content;
}
}
}

// hover 激活态
@mixin hover-active {
cursor: pointer;
transition: opacity 0.2s;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
}

styles/global.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}

html {
font-family: $font-family;
font-size: $font-size-base;
color: $text-primary;
-webkit-text-size-adjust: 100%;
}

body {
background: $bg-color;
line-height: 1.5;
}

a {
color: $primary;
text-decoration: none;
}

img {
max-width: 100%;
height: auto;
}

十一、日常速查口诀

1
2
3
4
5
6
7
变量命名 $ 开头,颜色间距一把抓
嵌套靠 & 连父级,最多三层别太深
重复代码抽 @mixin,传参灵活用 @include
不要参数的用 %placeholder,@extend 继承不重复
生成工具类 @each 写,Map 搭配最省事
数值计算 @function,px 转 rem 一招鲜
全局注入靠 Vite,@use 引入不污染

全文覆盖 Vue3 样式块四种模式、SCSS/Less 嵌套/Mixin/循环/函数/继承全部特性、三文件生产级模板、两种预处理器的差异对照表,以及日常速查口诀