Vite 迁移 Sass:从 @import 升级到 @use 的那些坑


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";`,
    },
  },
},

表面上看它可以让所有文件都访问变量,似乎很省心。

但实际会带来这些问题:

  1. 又把 @import 请回来,弃用警告会重新出现
  2. 每个 .scss 文件都会被重复注入——包括 theme/*.scssglobal.scssreset.scss 本身,会造成递归/自引/重复
  3. 主题文件和组件文件出现隐式耦合
  4. 组件样式里不声明依赖,也能“偷偷”拿到别的变量
  5. 最容易被忽略的一条:如果 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 侧只做一件事——拨 htmldata-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 步,业务组件零改动:

  1. theme/<名字>/index.scss,在 [data-theme="<名字>"] 下声明 --color-* 色板;
  2. 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,核心不是“替换语法”,而是把项目从“隐式全局依赖”调整为“显式模块依赖”。

最容易踩坑的地方有三处:

  1. 变量没有显式 @use
  2. @use 写在错误位置
  3. additionalData 回到全局注入的老习惯

而如果你要做主题系统,最稳妥的方案不是 Sass 变量全局注入,而是:

CSS 变量 + @use 模块化依赖。

这样既能保持 Sass 的可维护性,也能顺利支持运行时换肤。


9. 结合当前项目的落地结论

当前项目的主题令牌位于 theme/common.scss:root,主题注册链路是:

main.ts → reset.scss → @use "./theme/index"

其中 theme/index.scss 负责聚合 commonstar-purpleobsidian-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 模块边界清晰。


文章作者: 弈心
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 弈心 !
评论
 上一篇
Vite 环境变量前缀与密钥边界:为什么 `VITE_` 不是加密 Vite 环境变量前缀与密钥边界:为什么 `VITE_` 不是加密
解释 Vite 中 `VITE_` 前缀的真实含义、为什么它并不等于“安全”,以及如何正确区分公开配置和服务端密钥。
2026-09-19
下一篇 
Vite+Electron 必看:process 与 globalThis.process 到底是不是同一个?(彻底理清环境差异) Vite+Electron 必看:process 与 globalThis.process 到底是不是同一个?(彻底理清环境差异)
解释 Electron 主进程、渲染进程和 Vite 构建时 `process` 的真实来源,说明为什么在浏览器里直接使用 `process` 通常是错误做法,并给出现代 Electron 的安全实践。
2026-09-19
  目录