Vue3 Vite 这个组合近几年基本成了前端新项目的默认起手式SCSS 又几乎是团队协作里跑不掉的样式方案。很多人以为在 Vite 里用 SCSS 就是装个sass依赖的事真上手才发现事情没那么简单additionalData到底该不该用、use和import混用为什么疯狂告警、第三方组件样式死活覆盖不掉、vite build之后 CSS 体积膨胀……这篇我就把从零配置到工程化落地的完整路径捋一遍把那些文档里不会明说、但实际项目里一定会撞上的细节都翻出来讲清楚。适合正在搭新项目的人、想把老项目迁移到 Vite SCSS 的人以及被各种样式问题折腾过但没系统理过一遍的同学。1. 动手之前先想清楚 Vite 里 SCSS 的定位和隐藏规则1.1 原生 CSS 不够用的场景SCSS 到底补了什么如果你只是写个简单的落地页原生 CSS 配 CSS 变量完全够用没必要引入 SCSS。但只要是稍微有点规模的项目——几十个组件、多套主题、复杂的响应式断点、需要统一管理间距和颜色——SCSS 的价值就体现出来了。变量variable解决的是“同一个颜色值在 20 个文件里重复出现”的维护问题嵌套nesting让 DOM 结构和样式结构保持对应读代码的时候不用跳来跳去混入mixin能把那些带参数、带逻辑的公共样式抽出来复用尤其是处理flex居中、文本溢出省略、clearfix 这类高频片段函数function能做更精细的计算和控制配合each、for这类控制指令批量生成样式类就变得非常轻松。但要注意一个本质变化Vite 不是 webpack 那种把所有东西都塞进 JS 模块系统里处理的方式。Vite 对 CSS 的处理是“原生的、基于浏览器 ESM 的”SCSS 文件会被预处理器单独编译成 CSS再交给 Vite 的 CSS 管道处理。这个区别决定了后面所有配置逻辑。1.2 Vite 对 SCSS 的编译链路和 webpack 时代完全不同webpack 时代用sass-loader你要关心 loader 顺序style-loadercss-loadersass-loader顺序错了直接报错。Vite 把这些封装进了内置的 CSS 处理流程你用css.preprocessorOptions这一个配置项就能搞定。Vite 内部默认使用sass 编译器目前推荐安装的是sass或sass-embedded通过preprocessorOptions.scss向 SCSS 编译过程传参。也就是说你在vite.config.ts里写css.preprocessorOptions.scss的配置本质上是传给 sass 编译器的选项。很多新手搞不清scss和sass选项的区别scss对应.scss文件的编译配置sass对应缩进语法.sass的编译配置。国内项目里 99% 都用scss语法所以你只需要关心scss这个 key。理解了这条链路再去看网上一堆乱七八糟的配置教程你就能自己判断哪些是对的、哪些是过时的。2. 项目接入 SCSS 的完整实操从安装到全局变量生效2.1 正确安装依赖以及 sass 和 sass-embedded 怎么选Vite 官方文档说的是“安装 sass 即可”但这里有一个版本上的讲究。npm install -D sass # 或者 npm install -D sass-embedded两个包之间的关系简单说sass-embedded是官方基于原生编译器的嵌入式版本编译速度更快sass是纯 JS或 Dart实现的版本兼容性更好、生态更成熟。如果你在 Windows 上开发或者团队里有人用老版本 NodeVite 5 要求 Node 18这都不是问题直接装sass就好。我自己的建议新项目直接装sass。原因很实际——目前绝大多数团队、CI 环境、组件库的文档默认的都是sass遇到问题搜到的解决方案也多。sass-embedded虽然快但某些场景下和 Vite 的缓存机制配合还不够丝滑没必要为了那几十毫秒去冒险。装完依赖可以用如下命令验证是否安装成功npx sass --version能看到版本号就说明编译器可用。这个检查很重要因为很多项目报 “Legacy JS API is deprecated” 之类的警告根因就是版本新旧混用。2.2 最小可用配置在 vite.config.ts 里开启 SCSS 支持新建一个 Vite Vue3 项目后vite.config.ts默认长这样import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()] })要让 SCSS 正常工作按说不需要加任何配置——只要你在style langscss里写代码Vite 会自动调 sass 编译。真正需要配置的地方是你想注入全局样式资源的时候。比如你有一个src/styles/variables.scss文件里面放了一堆颜色变量和间距变量希望在每个组件的style langscss里都能直接使用而不是每个文件写一遍use /styles/variables.scss。这个需求用css.preprocessorOptions.scss.additionalData实现import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, css: { preprocessorOptions: { scss: { additionalData: use /styles/variables.scss as *; } } } })注意这里我用的是use而不是import这个区别放到后面第 3 节专门讲。as *的意思是把这个模块里的所有变量、混入、函数都以全局命名空间的方式注入。如果你不希望污染全局也可以用use /styles/variables.scss as vars;然后通过vars.$primary-color这种形式访问。2.3 additionalData 的副作用以及为什么不建议在这里放全量样式additionalData很省事但千万别把它当成“把所有 SCSS 都塞进去”的入口。我见过有人把整份index.scss里面包含大段的 reset 样式、全局 class写进additionalData结果每个组件的样式编译时都带着这一大坨重复代码vite build之后 CSS 文件里全是重复的 reset 样式和工具类。这里有个关键认知additionalData注入的内容是拼接在每个组件样式文件最前面参与编译的所以它适合放“纯变量、纯函数、纯混入”这类不会产生实际 CSS 输出的内容。一旦你往里面放任何会编译出 CSS 规则的代码比如.class {}、body {}这份 CSS 会被重复输出 N 次。正确做法是全局变量、混入、函数通过additionalData注入或者每个文件显式use。reset 样式、全局基础样式放在src/styles/index.scss里在main.ts通过import /styles/index.scss引入一次即可。src/styles/ ├── variables.scss # 变量被 additionalData 全局注入 ├── mixins.scss # 混入被 additionalData 全局注入 ├── functions.scss # 函数被 additionalData 全局注入 └── index.scss # reset 全局类只在 main.ts 引入一次三个被全局注入的文件里绝不能有实际的 CSS 规则只有变量定义、mixin、function定义。index.scss里才能写选择器和样式规则。这样划分之后每个组件编译时只多拼了十几行变量/混入定义代码中一次CSS 产物干净可维护性也高。3. 真正的工程化用 use 替代 import 组织你的样式目录3.1 为什么 import 在 Sass 的新版本里已经是弃用状态如果你用过老版本的 SassDart Sass 1.79 之前一定熟悉import variables这种写法。但从 Sass 1.80.0 开始import的弃用警告变得越来越频繁新版本甚至已经计划彻底移除。原因是import有几个致命问题重复加载同一个文件被多个文件importCSS 里会重复出现这份代码编译产物膨胀。全局污染import进来的变量、函数、混入全部暴露在全局命名空间两个文件里定义了同名变量后者会静默覆盖前者排查起来非常痛苦。无法确定依赖关系你不知道某个变量到底是哪来的重构时完全无从下手。use的规则不同每个文件作为一个模块无论被use多少次只加载并编译一次通过命名空间隔离变量和混入避免冲突依赖关系显式清晰。我自己在迁移老项目的时候被import的重复输出坑过一个公用的按钮混入被 10 个组件引用编译出来按钮样式重复出现了 10 份vite build之后光这一个问题就给 CSS 贡献了 40 多 KB 的冗余。换成use之后这部分直接降为一份效果立竿见影。3.2 一套可复用的 SCSS 目录结构以及模块之间的引用关系实际项目中我通常把 SCSS 按“基础层 - 业务层”两层拆目录长这样src/styles/ ├── fondations/ │ ├── _variables.scss # 变量颜色、字号、间距、圆角、阴影、断点 │ ├── _mixins.scss # 混入flex 布局、文本溢出、伪元素、媒体查询 │ └── _functions.scss # 函数单位转换、颜色计算 ├── base/ │ ├── _reset.scss # reset 样式 │ └── _global.scss # 全局公共类、body 基础样式 ├── components/ # 组件级样式按业务模块再分子目录 │ ├── button.scss │ └── dialog.scss ├── index.scss # 入口文件用 use 汇总导入 └── variables.scss # 兼容层不直接删掉注意文件名最前面的下划线_variables.scss是 Sass 的约定下划线开头的文件被认为是“部分文件”partial不会被单独编译成 CSS 文件只作为模块被其他文件use。这个约定配合模块化非常顺手。index.scss示例use ./fondations/variables; use ./fondations/mixins; use ./fondations/functions; use ./base/reset; use ./base/global;组件里使用的时候如果是局部业务样式显式 use 依赖use /styles/fondations/mixins as *; use /styles/fondations/variables as vars; .card { include flex-center; background: vars.$color-bg; }如果你已经在 Vite 的additionalData里全局注入了变量和混入组件里其实不需要写这行use就能直接用。但我个人还是倾向于显式 use因为代码的可读性和可追溯性比省一行代码重要得多。被additionalData注入的模块对开发工具比如 VS Code 的 Stylelint 插件来说往往不可见导致编辑器里一堆波浪线警告体验非常难受。3.3 变量、混入、函数与暗色主题的联动设计变量定位清楚了主题切换就好做了。这里分享一个我实际项目里用下来的模式用 SCSS 变量作为“设计令牌”design token通过给根节点加class来切换主题。// _variables.scss :root { --color-primary: #1890ff; --color-bg-base: #ffffff; --color-text-base: rgba(0, 0, 0, 0.88); --spacing-page: 24px; --radius-base: 8px; } html[data-themedark] { --color-primary: #1677ff; --color-bg-base: #141414; --color-text-base: rgba(255, 255, 255, 0.85); --spacing-page: 24px; --radius-base: 8px; }你可能会问这不是 CSS 变量吗和 SCSS 有什么关系这正是我踩过坑之后总结出来的原则——SCSS 负责“构建时”的静态组织和复用CSS 变量负责“运行时”的动态切换。SCSS 变量在编译后不存在于 CSS 产物里无法在运行时切换CSS 变量可以随时通过 JS 改变。所以我的原则是静态常量间距、字号、圆角这类不随主题变化的用 SCSS 变量管理保证编码期一致。动态主题变量颜色这类会随主题变化的用 CSS 变量管理配合>// _variables.scss $color-primary: var(--color-primary); // SCSS 变量引用 CSS 变量这样组件里写background: $color-primary既能享受 SCSS 变量的书写便捷又保留了运行时的动态性。实测在 Vue3 项目里这个方案非常稳。4. 构建与排错CSS 提取、浏览器兼容和常见告警的处理4.1 开发阶段的样式热更新与构建阶段的 CSS 产物优化Vite 开发模式的 HMR 对 SCSS 的处理非常顺滑——改一个变量所有引用它的组件样式都会自动刷新这比 webpack 时代的体验好太多。但有一个细节容易忽略开发阶段的样式是运行时注入style标签的构建阶段才抽取为独立 CSS 文件。如果你想控制构建阶段的 CSS 行为有两个关键配置export default defineConfig({ build: { cssCodeSplit: true, // 默认 true按需拆分为多个 CSS 文件 cssTarget: chrome61, // 明确目标浏览器避免不必要的 postcss 转换 }, css: { preprocessorOptions: { scss: { silenceDeprecations: [legacy-js-api], // 关闭 legacy API 告警 } } } })cssCodeSplit这个我一般保持默认。它会把异步组件对应的样式拆成独立的 CSS chunk随组件按需加载。如果你把所有样式都打进一个文件首屏要下载的 CSS 会很大白屏时间明显变长。cssTarget是我在排查过几个诡异问题后才注意到的Vite 默认会根据browserslist配置决定 CSS 是否需要降级处理。如果你的构建目标里包含老浏览器Vite 会对 CSS 做额外的兼容处理比如::placeholder的写法转换这会增加产物体积。如果你的项目只面向现代浏览器明确设置cssTarget可以避免一部分多余转换。4.2 我在实际项目里反复踩过的 SCSS 报错与修复记录把遇到的报错分为下面三类整理出来并按频率排序。第一类Mixed Declarations 问题Error: expected }. ╷ │ .foo { │ color: red; │ include some-mixin; // 报错指向这里 │ }这类错误的经典原因是旧版 import 的混入与普通声明混写时sass 解析顺序出问题。如果你还在用import并且include位置在嵌套规则中间很容易触发。解决方案升级为use语法并把include放在属性的前面。.foo { include some-mixin; // 先 color: red; // 后 }第二类undefined variable 警告甚至编译失败Error: Undefined variable.这个报错 90% 的情况是文件作用域问题。你也许已经配置了additionalData但这个注入只对 Vue 单文件组件的style块生效对 src 目录下独立的.scss文件不生效。换句话说Button.vue里写style langscss能访问全局变量因为 Vite 的additionalData拼接进去。src/styles/button.scss里直接写$color-primary则不行因为这个文件不是通过 Vue SFC 编译的additionalData没有拼进去。解决方案独立.scss文件里自己加一行use /styles/variables.scss as *;别依赖全局注入。第三类import 的弃用告警Deprecation Warning: Sass import rules are deprecated and will be removed in Dart Sass 3.0.0这个我之前提过解决思路很直接把import换成use。如果你遇到用下面的自动替换命令npx sass-migrator module --migrate-deps --load-pathsrc ./src/styles/index.scsssass-migrator是官方提供的迁移工具它会把import指令自动转成use并补上缺失的命名空间。不过它也不是万能的如果你的代码里大量依赖隐式全局变量名迁移后可能会报 undefined 变量这时候需要手动在文件开头补上use。4.3 第三方组件库样式覆盖不掉怎么用 SCSS 的插值语法解决Vue3 项目基本离不开 Element Plus、Ant Design Vue 这类组件库。它们一般提供两种自定义方式CSS 变量覆盖、深度选择器覆盖。但你在 SCSS 里覆盖时经常会遇到“类名被 postcss 打上了>.el-button { :deep(.el-icon) { margin-right: 4px; } }编译后变成.el-button[data-v-xxxx] .el-icon { margin-right: 4px; }这能解决大多数覆盖问题。但有些组件库的样式是通过use引入了它们自己的 SCSS 变量你再怎么覆盖类名都没用因为它们内部用的变量是硬编码的。这时候可以用 SCSS 的!default机制来定制。比如 Element Plus 支持通过 SCSS 变量定制主题最核心的是你在自己的变量文件里覆盖它暴露的变量// src/styles/element-variables.scss forward element-plus/theme-chalk/src/common/var.scss with ( $colors: ( primary: ( base: #409eff, ), ) );然后在vite.config.ts里把这个文件加到additionalDatacss: { preprocessorOptions: { scss: { additionalData: use /styles/element-variables.scss as *; use /styles/variables.scss as *; } } }注意这里用forward ... with (...)的写法它允许你在导入模块之前修改对方定义的!default变量。这是新版 Sass 推荐的主题定制方式也是我调了好久才摸出来的路径。如果你用老写法import element-plus/packages/theme-chalk/src/index.scss;再改变量在新版本下会直接报错。4.4 一个真实案例pxtorem 对 ECharts 不生效不是 SCSS 的锅热搜词里有一条是 “pxtorem 对 echarts 没起到效果 vue3”这个我遇到过值得说一嘴。问题是这样的页面里用了 ECharts 图表你按设计稿把容器宽度写成了400px期望项目里的postcss-pxtorem插件在构建时把它转成rem结果图表宽度死活不随根字号响应。排查时发现postcss-pxtorem默认会忽略node_modules里的样式和某些内联样式ECharts 的canvas尺寸是通过 JS 计算并设置canvas标签的width属性根本不是 CSS 像素单位当然不会被 pxtorem 转换。解决方案是监听窗口变化后调用chart.resize()并在 resize 时重新读取容器当前宽度已经是转换后的 rem 换算值。SCSS 在这里的作用只是提供容器宽度变量真正的响应式逻辑归 JS 管。const resizeHandler () { chart?.resize() } window.addEventListener(resize, debounce(resizeHandler, 200))这类问题属于典型的“方案定位错误”不是 SCSS 的问题但如果你在排查过程中对样式链路不清晰很容易绕进去半天出不来。5. 样式组织与维护SCSS 使用中容易被忽略的几条硬规则5.1 命名规范BEM 和 SCSS 嵌套怎么结合才优雅SCSS 的嵌套语法很舒服但容易让人掉进“无限制嵌套”的陷阱。嵌套层级过深超过 4 层会导致编译后的选择器非常长最深层选择器可能达到.header .nav .menu .item .link-text这种可怕的长度。浏览器匹配选择器时从右往左深嵌套虽不至于有明显性能问题但维护时看一坨嵌套真的很崩溃。组件复用性变差样式和 DOM 结构强耦合DOM 一调整样式就失效。我这一年多坚持的是 BEM 嵌套的一层或两层结合.card { __header { display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-page); } __title { font-size: 16px; font-weight: 600; } --active { border-color: var(--color-primary); } }这样编译出来是.card__header、.card__title、.card--active结构清晰、类名单一、不会被深层嵌套拖累。混合--和元素__的组合配合引用父选择器写起来也很顺手。5.2 什么时候该放弃 SCSS 变量改用 CSS 变量前面说过了SCSS 变量是构建期概念。当一个值的变更需要在运行时例如用户切换主题、点击切换密度、响应式断点动态调整发生SCSS 变量无能为力必须用 CSS 变量。再一个判断维度是作用域。CSS 变量天然具备“继承”和“局部覆盖”能力比如你想让某个卡片组件内部的间距整体变小只需要在.card上重新定义--spacing: 16px内部所有引用这个变量的地方自动更新。SCSS 变量做不到这一点它是编译期的“全局常量”想局部改就必须改源码重新编译。但 CSS 变量有个缺陷不支持混入和函数。你想封装一个根据基准值计算响应式字号的逻辑CSS 变量实现起来很别扭SCSS 的function和each才是干这个的。所以我在项目里遵循这几个判断标准需要参与运算、生成衍生值的 → SCSS 变量。需要动态切换主题的 → CSS 变量。需要局部作用域覆盖的 → CSS 变量。需要在media查询里逻辑判断的 → SCSS 变量加控制指令。5.3 让开发体验更顺的小配置Stylelint 与编辑器提示SCSS 写多了最常见的错误是变量名拼写错误、重复声明、选择器嵌套过深。这些在编译期不一定报错但结果就是样式不如预期。磨刀不误砍柴工给项目配上 Stylelint 之后这些问题能在编辑器里直接标红。npm install -D stylelint stylelint-config-standard-scss stylelint-order.stylelintrc.json示例{ extends: [stylelint-config-standard-scss], plugins: [stylelint-order], rules: { order/properties-alphabetical-order: null, order/properties-order: [ position, top, right, bottom, left, display, align-items, justify-content, margin, padding, width, height, border, background, font-size, color ], scss/dollar-variable-pattern: null, scss/at-use-no-undefined: true } }这里scss/at-use-no-undefined是我强烈建议开的一条规则它能直接检查use的路径是否存在、被引入的变量是否真的定义了避免一进组件就遇到 undefined 变量报错的问题。顺序规则properties-order看起来有点强迫症但对团队协作很有价值——大家写的属性顺序一致diff 的时候不容易因为位置差异产生噪音。5.4 伪类与区分 hover、focus、active 状态在 SCSS 里的经验交互样式在 SCSS 里容易漏掉状态区分。很多人只写了:hover忘了:focus-visible键盘用户完全没法操作。我建议在混入里把常用的状态组合封装起来mixin interactive-state { :hover { content; } :focus-visible { content; } :active { content; } }用法.btn { include interactive-state { border-color: var(--color-primary); color: var(--color-primary); } }这样写鼠标悬停、键盘聚焦、按下三种状态一次搞定不会漏。真做了无障碍适配的人会懂这个细节多重要。6. 关于 SCSS 使用范围我最后的几点实践经验Vite SCSS 这套组合我从 Vue3 项目搭建到后台管理系统、可视化大屏都验证过稳定性和效率都在线。比起 webpack 时代配置量少了一个量级你要学的核心就三件事安装依赖、配置additionalData、用use管好模块关系。把这三件事理顺剩下的大多数问题都只是业务层面的取舍。最后分享一个我个人的体会样式方案最容易失控的地方永远不是语法而是“全局不管是”。如果项目刚起步时就定好变量文件、目录分层、BEM 命名规则后续的维护成本会低很多如果等代码写到两万行再回来治理那真的要脱一层皮。所以我建议新项目在第一周就把这些规范固化到 Stylelint 和目录约定里逼着团队从一开始就按统一的方式写。还有一个小技巧如果你发现自己经常要在“全局 SCSS 变量”和“局部组件样式”之间来回跳可以把additionalData里注入的_variables.scss文件精简到极致只保留真正全局共用的变量。其余 80% 的业务变量放在各自业务模块的局部 SCSS 文件里像函数参数一样显式传入。这样每个 SCSS 文件看第一眼就知道它依赖什么整个项目的样式依赖图是清晰透明的而不是所有东西都被迫挂在一棵“全局变量树”上。这一条经验是我在维护一个三万多行 SCSS 的老项目时最想穿越回去告诉当年的自己的。