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
| export default defineConfig({ css: { preprocessorOptions: { scss: { 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 } } }
|
| 配置项 | SCSS | Less |
|---|
| 路径别名 | ~@/ 或 @/ | ~@/ 或 @/ |
| 全局注入 | additionalData | additionalData |
| 数学运算 | 默认 / 是除法 | 需 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 { &__header { ... } &__body { ... } &__footer { ... }
&--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
| @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
|
$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 变量语法差异:
| 特性 | SCSS | Less |
|---|
| 定义 | $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
| :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
| @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
| .flex(@direction: row, @justify: flex-start, @align: stretch) { display: flex; flex-direction: @direction; justify-content: @justify; align-items: @align; }
.header { .flex(row, space-between, center); }
|
| 特性 | SCSS Mixin | Less Mixin |
|---|
| 定义 | @mixin name($param) { } | .name(@param) { } |
| 调用 | @include name(val) | .name(val); |
| 默认参数 | $param: default | @param: default |
| 可变参数 | $params... | @params... |
| 条件判断 | @if / @else | when 守卫 |
五、循环生成 —— 批量创建工具类
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, );
@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, );
@each $name, $color in $theme-colors { .bg-#{$name} { background-color: $color; } .text-#{$name} { color: $color; } .border-#{$name} { border-color: $color; } }
@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; } }
|
5.5 Less 循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @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
| @function rem($px) { @return calc($px / 16) * 1rem; }
@function vw($px) { @return calc($px / 375) * 100vw; }
.title { font-size: rem(24); padding: vw(16); }
|
6.2 颜色透明度
1 2 3 4 5 6 7 8
| @function alpha($color, $opacity) { @return rgba($color, $opacity); }
.overlay { background: alpha(#1890ff, 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
| %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 | 块级作用域,不会被覆盖 |
| Map | map-get($map, key) | 键值对结构 |
| List | nth($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-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; } } }
@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/循环/函数/继承全部特性、三文件生产级模板、两种预处理器的差异对照表,以及日常速查口诀。