Vite 迁移 Sass:从 @import 升级到 @use 的那些坑
场景:Electron 桌面 + Vue 3 + Vite +
sass@1.103(当前最新)。老项目把「主题样式」用@import全局引入,迁移到@use后踩了几个坑,把结论沉淀下来。
Sass 的迁移并不只是把 @import 换成 @use。真正重要的是:
@import是“全局拼接”,而@use是“模块化依赖”。
如果直接把旧方案照搬过来,最常见的结果就是:明明看起来变量已经定义好了,编译器却报 Undefined variable,或者主题文件被重复注入。
这篇文章总结了我在 Vite + Vue + Sass 工程中的几处典型踩坑,以及更稳妥的落地方案。
1. 先说结论:@import 和 @use 不是同一套语义
在旧写法中,很多人会把样式文件直接 @import 成一大堆:
// common.scss
$font-size-md: 14px;
$color-primary: #3b82f6;
@import "./theme/dark";
@import "./theme/light";
这样做的好处是:全局变量可以直接用,主题也能一次性拼进去。
但它的毛病也很明显:
- 依赖隐式
- 语义不清
- 主题和基础变量强耦合
- 未来升级时拿不到清晰的可维护性
@use 的思想和它完全不同:成员默认不是全局可见的,而是按模块声明依赖。
也就是说:
谁需要这个成员,谁就显式
@use它。
这正是 @use 的核心设计。
2. 坑一:成员不再全局可见,变量就会“看起来存在,实际上不存在”
迁移前最容易踩的坑,是在其他文件里还直接写:
width: $spacing-md;
但对应模块并没有在当前文件中 @use,于是编译器报:
Error: Undefined variable.
这里的关键是:
@import:变量是“全局合并”的@use:变量是“模块作用域”的
所以最稳妥的写法是:
@use "@/assets/css/theme/spacing" as *;
或者:
@use "@/assets/css/theme/spacing" as spacing;
然后用命名空间来访问:
width: spacing.$spacing-md;
这就是 @use 的本质:依赖必须显式声明。
2.1 关于别名 @ 的解析
Vite 项目里通常配了 @ → renderer(或 @ → src)这样的路径别名。@use "@/assets/..." 在 Vite 里是能解析的——Vite 的 sass 集成会把 resolve.alias 传给 sass importer。
但更稳的写法是优先用相对路径,尤其在 index.scss 这类聚合文件、或多层目录里,避免别名在不同工具链 / CI 下解析差异:
// 用相对路径,解析行为更可预期
@use "./common";
@use "./star-purple"; // 目录 → 自动解析到 star-purple/index.scss
@use "./obsidian-gold";
别名能用,但相对路径能让你在更换构建器或跨机 CI 时少踩一个坑。
3. 坑二:@use 必须放在文件最前面
@import 往往可以出现在任意位置,旧项目里常见的写法是把它放在底部。
但 @use 不能这样:
它必须写在任何样式规则和普通声明之前。
如果这样写:
body {
color: red;
}
@use "./theme";
就会触发:
Error: @use rules must be written before any other rules.
因此,一旦迁移到 @use,建议统一把依赖声明放到文件顶部:
@use "./theme";
@use "./mixins";
body {
color: red;
}
这样的组织方式最清晰,也最易维护。
4. 坑三:additionalData 不是“解药”,而是“另一个坑”
很多人迁移时会想,既然 @use 不方便,那就继续用 Vite 的全局注入:
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/assets/css/common.scss";`,
},
},
},
表面上看它可以让所有文件都访问变量,似乎很省心。
但实际会带来这些问题:
- 又把
@import请回来,弃用警告会重新出现 - 每个
.scss文件都会被重复注入——包括theme/*.scss、global.scss、reset.scss本身,会造成递归/自引/重复 - 主题文件和组件文件出现隐式耦合
- 组件样式里不声明依赖,也能“偷偷”拿到别的变量
- 最容易被忽略的一条:如果
common.scss里带了 CSS 规则(旧项目里常是一大坨html[data-theme]{...}主题块),那这些规则会被注入到每一个 .scss 的产物里,每个组件的 CSS 都重复输出,体积膨胀,主题块被复制 N 遍
这和 @use 的设计完全背道而驰:
@use是让依赖显式化,而additionalData又偷偷把隐式依赖做回来了。
核心矛盾在于:additionalData 解决的是「Sass 变量全局可见」,但主题根本不需要——主题走 CSS 变量的级联就能全局生效。
所以,除非你非常明确地要“全局注入某个小工具文件”,否则不要把 additionalData 当作迁移方案。
5. 正解:主题最好用 CSS 变量,而不是 Sass 变量
最关键的设计原则是:
主题切换属于运行时行为,应该走 CSS 变量;编译期常量才适合 Sass 变量。
很多人会把主题色直接写成 Sass 变量:
$color-primary: #536dfe;
但这里的痛点在于:主题切换要在浏览器运行时切换 data-theme,而 Sass 变量在编译时就已经固定了,无法完成动态切换。
更合理的做法是:
:root {
--color-primary: #536dfe;
}
[data-theme="dark"] {
--color-primary: #7c9dff;
}
组件里统一使用:
.card {
background: var(--color-primary);
}
这样做的好处很明显:
- 主题切换无需 JS 处理颜色值
- 组件不绑定某个固定 Sass 变量
- 不需要任何全局注入
- 语义更清晰,运行时更灵活
这也是现代前端主题系统中最稳妥的设计。
Sass 变量 vs CSS 变量,一张表看清
Sass 变量($f12) |
CSS 变量(--font-size-sm) |
|
|---|---|---|
| 编译期 / 运行时 | 编译期常量,输出即定死 | 运行时,级联可覆盖 |
能按 data-theme 切换吗 |
❌ 不能 | ✅ 能 |
| 全局可见需要注入吗 | 需要(@use / additionalData) | 原生级联继承,不需要 |
| 能参与 Sass 计算(darken 等)吗 | ✅ | ❌(需 CSS 新语法) |
要注意这条反向约束:CSS 变量不能参与 Sass 编译期计算(如 darken())。需要计算的量,才留给 Sass 变量。所以两者各管一段——运行时主题用 CSS 变量,编译期常量用 Sass 变量,而不是二选一。
JS 侧只做一件事——拨 html 的 data-theme 属性,颜色值一个都不碰:
const applyTheme = () => updateHTMLAttrs(theme.value); // html[data-theme=obsidian-gold]
5.1 字号令牌的取舍(可复用到其他项目)
把”编译期常量 vs 运行时主题”再落到一个具体、可借鉴的例子——字号阶梯如何选型:
- 优先把字号做成 CSS 变量:在
:root声明--font-size-xs、--font-size-sm…,靠级联全局使用,不需要任何注入。 - 只有当字号需要参与 Sass 编译期计算时,才单独建一个无 CSS 输出的
_sizing.scss,并在使用处显式@use。 - 两种令牌可以并存,但不要再次依赖隐式全局变量。
判断依据就一条:这个值是否需要在运行时变化、或被组件直接引用?是 → CSS 变量;不是且参与编译期计算 → Sass 变量。
6. 一个更推荐的项目结构
如果是 Vite + SCSS 的标准项目,可以整理成:
src/assets/css/
├── base.scss
├── theme/
│ ├── index.scss
│ ├── common.scss
│ ├── light.scss
│ └── dark.scss
├── utils/
│ └── spacing.scss
└── app.scss
主题入口:
@use "./theme/common";
@use "./theme/light";
@use "./theme/dark";
业务组件中,只在真正需要的地方显式 @use:
@use "@/assets/css/utils/spacing" as *;
这样结构会更清晰:
- 主题统一管理
- 设计令牌集中维护
- 组件依赖显式声明
- 不再混用隐式全局依赖
新增一个主题只需 2 步,业务组件零改动:
- 建
theme/<名字>/index.scss,在[data-theme="<名字>"]下声明--color-*色板; - 在
theme/index.scss主题注册表里补一行@use "./<名字>";。
组件统一用 var(--color-*),换肤零改动。
7. 迁移时的几个注意事项
7.1 别把 @import 继续“偷偷挂回去”
有些项目会通过 silenceDeprecations 来压住告警,但这只是掩盖症状,不是根治问题。
如果你已经决定迁移到 @use,最稳妥的方式不是压住警告,而是让所有文件都遵循模块化规则。
7.2 @use 和 CSS 变量不是对立关系
它们是不同层面的东西:
@use:处理 Sass 模块依赖- CSS 变量:处理运行时主题和动态样式
合理的方案是:
- 编译期常量走
@use - 动态主题走 CSS 变量
7.3 不要为了省事而把所有东西都放进全局入口
一旦全局入口文件变得巨大,后面维护和问题排查就会非常痛苦。更好的方法是:
- 组件各自走显式依赖
- 公共设计令牌集中纳管
- 主题文件单独负责色板和状态变量
7.4 两个容易被忽略的配置澄清
- 不需要 sass-loader。那是 webpack 的专属加载器;Vite 原生用
sass包编译,只配css.preprocessorOptions.scss即可,装了最新sass就能跑。 quietDeps: true可以留。它只剔除node_modules里第三方 sass 的告警噪音,和上文”别用silenceDeprecations掩盖自己的@import“是两个不同问题——一个是压别人的无关告警,一个是藏自己的问题。
8. 最后总结
迁移到 @use,核心不是“替换语法”,而是把项目从“隐式全局依赖”调整为“显式模块依赖”。
最容易踩坑的地方有三处:
- 变量没有显式
@use @use写在错误位置- 用
additionalData回到全局注入的老习惯
而如果你要做主题系统,最稳妥的方案不是 Sass 变量全局注入,而是:
CSS 变量 +
@use模块化依赖。
这样既能保持 Sass 的可维护性,也能顺利支持运行时换肤。
9. 结合当前项目的落地结论
当前项目的主题令牌位于 theme/common.scss 的 :root,主题注册链路是:
main.ts → reset.scss → @use "./theme/index"
其中 theme/index.scss 负责聚合 common、star-purple 和 obsidian-gold。根级 common.scss(包含 $f20、$f12 等旧字号变量)已经移除,且项目中没有剩余引用,因此不需要为了恢复历史变量而重新启用全局注入。
9.1 不使用 additionalData 全局注入
additionalData 解决的是 Sass 成员的全局可见性,并不能解决主题切换问题。在每个 .scss 文件中注入一份包含 CSS 规则的文件,还可能造成重复输出、递归依赖和隐式依赖。
此外,@import 在 Sass 1.80+ 已被弃用。项目既然已经迁移到 @use,就不应再通过 additionalData 把 @import 引回来。silenceDeprecations: ["import"] 也只是抑制告警,不能替代迁移。
9.2 字号令牌的选择
如果后续需要恢复字号阶梯,优先在 theme/common.scss 中增加 --font-size-xs 等 CSS 变量。它们可以像主题色一样通过级联全局使用,不需要额外注入。
只有在字号需要参与 Sass 编译期计算时,才适合单独建立无 CSS 输出的 _sizing.scss,并在使用处显式 @use。两种令牌可以并存,但不应再次依赖隐式全局变量。
最终结论:
reset.scss只需加载一次 CSS 变量色板;组件使用var(--color-*),编译期常量再通过局部@use引入。这样既支持运行时换肤,也能保持 Sass 模块边界清晰。