先给结论:如果你还在新项目里用@import管理 SCSS 模块,那么你有必要认真看看这篇文章了。Dart Sass 官方已经明确@import是弃用功能,并且计划在 Dart Sass 3.0.0 中彻底移除。也就是说,你今天写的@import代码,在不久的将来就会直接编译报错,而不是仅仅给个 warning。这不是危言耸听,是正在发生的事情。
很多前端开发者对@use、@forward的印象停留在“新出的东西,好像能替代@import”,但实际真要动手改造项目时,要么不知道从哪下手,要么混淆了@use和@forward的职责。我自己在带团队做样式模块化改造时,就踩过不少坑,也和同事反复讨论过“这俩到底什么区别”这种问题。这篇文章就把这三者的机制、适用场景、迁移方法和实际坑位一次性聊透。
如果你已经用 SCSS 写过一段时间样式,或者正在维护一个样式文件多到爆炸的老项目,又或者刚接触 Vue3/Vite 生态想用上新的 SCSS 模块化能力,这篇文章就是给你准备的。
1. 为什么会有三种“引入”指令:背景与差异定位
1.1 从 CSS 预处理器的模块化困境说起
先聊点背景。CSS 本身没有变量、没有函数、没有作用域,写大型项目时很容易变成“一处改动,全局爆炸”。SCSS 的出现解决了这些问题,但早期的模块化手段只有@import一个。它的工作方式非常原始:把被引入的文件内容直接拷贝到当前文件的对应位置,然后一起编译。
听起来挺香?但实际上问题非常大。最典型的一个场景:你引入了两个文件,A 文件里定义了一个$color,B 文件里也定义了一个$color,两者值不一样,那么哪个文件后加载,哪个就覆盖前一个。你根本不知道最终的$color是谁,得靠猜、靠查代码顺序。这就是“全局命名空间污染”的典型症状——所有文件共用一个全局作用域,没有任何隔离。
@import的另一个坑是重复加载。如果一个文件被多个文件@import,那么这个文件的代码会被复制多份,编译产物里也会出现大量重复的 CSS 规则,最终打包体积直接膨胀。虽然 SCSS 的@import在 Sass 层面做了一些去重处理(同一文件只会加载一次),但实际使用中还是有很多边界情况会导致重复输出,尤其是在嵌套选择器里使用@import的时候,简直是一场噩梦。
而@use和@forward就是针对这些痛点设计出来的新方案。它们的核心思路是:每个文件都是一个独立模块,模块内部的成员(变量、混入、函数)默认不对外暴露,只有通过@use明确引入后才可以使用,并且通过命名空间隔离避免冲突。
1.2 核心机制对比:一行代码看出本质差异
先用一个最简单的例子把三者的使用形态摆出来。
// _reset.scss $base-color: #333; @mixin btn-style { padding: 8px 16px; border-radius: 4px; } // 老写法:@import @import 'reset'; .btn { @include btn-style; color: $base-color; } // 新写法:@use @use 'reset'; .btn { @include reset.btn-style; color: reset.$base-color; }仔细看这两段代码的区别:@use引入后,混入变成了reset.btn-style,变量变成了reset.$base-color,所有成员都挂在reset命名空间下。这样做的好处是:即使另一个文件里也有$base-color或者btn-style,也不会和reset下的成员冲突,因为调用时已经明确指定了来源。
而@forward干的事情更特别。它不把文件内容加载到当前作用域,而是把另一个文件的成员“转发”出去,相当于给下游用户开了一个中转站。它的典型场景是库开发。比如你开发了一个 UI 组件库,内部结构分了很多子模块,但又不想让用户一个一个地@use子模块路径,于是你创建一个index.scss文件,用@forward把所有需要公开的模块聚合到一起,用户只需要@use '你的库'就能一个入口拿全部。
顺带提一句题外话。很多写过 Python 的朋友第一次看到@import会本能地想到 Python 的 import 语句,误以为 SCSS 的@import和它是同类机制。实际上两者的语义差异非常大:Python 的 import 是运行时加载模块对象,有明确的命名空间和缓存机制;SCSS 的@import本质上是文本复制粘贴,没有任何作用域隔离。这也是为什么它必须被淘汰。
1.3 一张表搞懂三者的核心区别
| 对比维度 | @import | @use | @forward |
|---|---|---|---|
| 作用域隔离 | 无,全部成员进入全局作用域 | 有,默认生成命名空间 | 有,本身不暴露成员给当前文件 |
| 重复加载 | 可能产生重复代码 | 同一文件只加载一次 | 只转发不加载,无重复问题 |
| 调用方式 | 直接使用成员名 | 命名空间.成员名 | 不直接调用,面向下游 |
| 私有成员支持 | 不支持 | 支持,下划线开头的成员不导出 | 支持,可隐藏内部实现 |
| 配置变量 | 不支持 | 支持,配合!default | 支持,可透传配置 |
| 官方状态 | 已弃用,计划移除 | 推荐使用 | 推荐使用 |
| 典型场景 | 老项目遗留代码 | 普通项目模块化引入 | 组件库、样式库的聚合出口 |
这条表格不是让你背下来的,而是要理解背后的设计思路:@import是“无脑复制粘贴”,@use是“带命名空间的引入”,@forward则是“带转发能力的再导出”。三者中@use是日常开发的主力,@forward是库开发者手里的利器,而@import是应该被扫进历史堆的旧方案。
2. 拆穿 @import 的旧账:它到底做错了什么
2.1 全局污染:变量相互覆盖的实际案例
全局污染是@import最让人头秃的问题。我举一个真实的翻车案例。之前接手过一个老后台项目,样式文件有三十多个,全是@import引入的。项目里有两个模块都定义了$primary-color,一个定义为蓝色偏亮,一个定义为蓝色偏暗。结果就是:页面有一部分用了亮的,有一部分用了暗的,看起来像是 UI 设计师精神分裂。排查原因花了整整一下午,最后发现是一个子文件里多了一行@import 'theme',把全局的$primary-color覆盖了。
这种问题是@import的天然缺陷:所有文件共享同一个全局命名空间,一旦成员名相同,后面加载的就会覆盖前面的。文件一多,追踪起来真的想砸电脑。而这种问题在用@use之后几乎不存在了,因为每个模块的成员都挂在各自的命名空间下,哪怕两个模块都叫$primary-color,使用时写清楚a.$primary-color和b.$primary-color就行,永远不会有覆盖问题。
2.2 重复加载与编译产物体积膨胀
@import的重复加载问题很容易被忽略。看一个经典的反面例子:
// _base.scss * { box-sizing: border-box; } // _panel.scss @import 'base'; .panel { border: 1px solid #ddd; } // _button.scss @import 'base'; .btn { display: inline-block; } // main.scss @import 'panel'; @import 'button';这段代码最终编译出来的 CSS 里,* { box-sizing: border-box; }会出现几遍?答案是两遍。因为panel和button各自@import了一次base,主文件又@import了这两个文件,Sass 的@import去重机制并不能完全避免这种嵌套场景下的重复输出。如果你的项目里有几十个这样互相嵌套的文件,编译出来的 CSS 里大量重复的规则会直接拖垮首屏加载速度。
这也是为什么很多老项目用上了 CSS 压缩工具、PurgeCSS(用于移除未使用 CSS 的工具)之后还是感觉样式文件很大——重复规则太多了,压缩工具能合并相同选择器,但没法自动识别哪些是真正“无用的重复”。
2.3 无法暴露私有成员,一切被迫公开
@import的另一个设计缺陷是没有私有成员的概念。你在_helper.scss里写的所有变量、混入,被@import之后全部变成全局可见的。这在团队协作中会造成混乱:A 同学写了一个工具混合,本来只打算自己模块内部用,结果 B 同学不知道,也在其他地方@use了它,后来 A 同学重构改了这个混合的名字,B 同学的页面样式就挂了。
而@use和@forward都支持下划线前缀的私有成员机制。只要成员名以-开头(比如$-private-color),这个成员就不会被导出,外部文件无论怎么@use都访问不到。这才是合理的模块边界:对外公开的接口是明确的,内部实现细节可以放心改动。
2.4 官方弃用时间线与迁移紧迫性
我知道很多人对“弃用”不敏感,觉得还能用就行。但这里要提醒一句:Dart Sass 在 1.80.0 版本中已经对@import发出了正式的弃用警告,并明确了移除计划。按照官方给出的时间表,Dart Sass 3.0.0 会彻底移除@import。以当前迭代速度来看,这个版本不会太远。
到那时候,所有老项目一次性升级时,将不得不面对大量编译报错。与其等到被动升级,不如现在就慢慢把项目里的@import替换成@use。而且,官方提供了一个迁移工具——sass-migrator,可以自动完成大部分迁移工作,这我在第 5 部分会专门讲。
3. @use 的正确打开方式:命名空间、私有成员与配置变量
3.1 默认命名空间:文件名即命名空间
@use最直观的变化就是命名空间。它默认使用被引入文件的文件名作为命名空间(不含下划线和扩展名)。举个例子:
// _theme.scss $font-size: 14px; @mixin center { display: flex; align-items: center; justify-content: center; } // main.scss @use 'theme'; body { font-size: theme.$font-size; } .box { @include theme.center; }这里theme就是默认命名空间。这样做的好处是:当你不确定一个变量是谁家的,看名字前缀就能找到来源,代码的可读性大大提升。
如果你觉得默认命名空间太啰嗦,可以用as来自定义:
// 自定义命名空间 @use 'theme' as t; body { font-size: t.$font-size; } // 取消命名空间,直接使用成员名 @use 'theme' as *; body { font-size: $font-size; }这里有一个需要特别注意的坑:as *会把模块的所有成员直接拉到当前作用域,这实际上又回到了类似@import的全局污染模式。虽然 Sass 允许你这么写,但我在实际项目中不建议频繁使用,除非你非常确定模块里的成员名不会和当前文件、其他as *模块冲突。用多了as *,你等于亲手把@use的优点丢掉了。
3.2 私有成员:下划线开头的命名约定
@use的私有成员机制很简单:凡是以_(单个下划线)开头的变量、混合、函数,都不会被导出。注意我这里用的是英文下划线_,不是减号。让我重新确认一下这个细节。
在 Sass 中,私有成员的定义方式是:成员名以_开头(例如$-color)。这个约定和 Sass 之前的“部分文件以下划线开头”是两回事——部分是文件名层面的约定,而私有成员是变量/混合/函数名字层面的约定。
// _config.scss $-private-color: #333; // 私有变量,外部无法访问 $public-color: #666; // 公开变量,外部可以访问 // main.scss @use 'config'; body { color: config.$public-color; // 正常 color: config.$-private-color; // 报错,私有成员不可访问 }如果你正在维护一个团队内部使用的样式库,私有成员机制能帮你把“对外 API”和“内部实现”彻底分开。外部用户可以依赖公开成员,而你可以放心重构内部私有成员而不必担心破坏下游代码。这本质上就是软件工程里的封装思想延伸到样式代码的体现。
3.3 配置变量:用!default与with实现按需定制
@use还支持模块级的配置功能,这是@import完全不具备的。配置功能的实现依赖两个机制:在被引入模块中使用!default设置默认值,在引入方使用with传入覆盖值。
// _theme.scss $primary-color: #007bff !default; $border-radius: 4px !default; // main.scss 局部覆盖配置 @use 'theme' with ( $primary-color: #ff6b6b, $border-radius: 8px ); body { color: theme.$primary-color; }这个机制对组件库和主题系统非常有用。你可以写一套带默认主题的组件样式库,下游用户在使用时通过with传入自定义的主题色、圆角、间距等变量,而不需要去改动库源码。这跟在构造器里设置默认参数,再由外部传入覆盖配置的设计思路如出一辙。
但要注意一个限制:with的配置必须在@use语句所在文件里一次性完成,而且同一个模块的配置是全局性的——如果你在多个文件里都@use了同一个模块并传了不同的with配置,Sass 会报错,报错信息大致是“Module loop: this module is already being loaded”。也就是说,同一个模块在一个编译任务里只能被配置一次,配置必须集中在入口文件里做。我在做主题切换功能时就遇到过这个问题:本想在不同页面文件里分别配置不同主题色,结果直接编译报错,最后只能把配置集中在入口文件里,通过运行时切换 class 来改变样式。这是@use的一個容易踩的坑,后面会详细说。
3.4 作用域与控制指令:@use 不允许嵌套使用
还有一个细节容易忽略:@use只能写在样式表的顶层,不能嵌套在选择器内部或条件规则内部。原因是@use在编译阶段就要确定模块关系和命名空间,它必须在编译的早期阶段完成模块解析。相比之下,@import虽然也可以嵌套在里面,但这种能力本身就是反模式,会让样式文件的依赖关系变得复杂难懂。
// 正确:顶层使用 @use 'theme'; // 错误:嵌套使用 .container { @use 'theme'; // 编译报错 }如果确实需要按条件加载不同样式,应该通过@if和@else结合@use的配置变量来实现,或者把条件逻辑下沉到模块内部由变量控制输出。
4. @forward 的存取一体术:库开发者的转发利器
4.1 @forward 的角色定位:给下游开一扇中转门
@forward和@use的关系,可以理解成“转发站”和“进口商”。@forward本身不把任何成员引入到当前文件的作用域中,它只负责“转发”——把别的模块的成员原封不动地转交给更下游的消费者。
这么说有点抽象,看一个最典型的使用场景。假设你在开发一个 UI 库,内部结构是这样的:
// styles/ // ├── _variables.scss // ├── _mixins.scss // ├── _buttons.scss // ├── _cards.scss // └── index.scss如果你用@import,用户要分别引入variables、mixins、buttons、cards,既繁琐又容易漏。用@forward,你可以在入口文件index.scss里做一层聚合转发:
// index.scss @forward 'variables'; @forward 'mixins'; @forward 'buttons'; @forward 'cards';下游用户只需要一行代码:
@use 'ui-library'; // 然后就可以用 // ui-library.$primary-color // ui-library.btn-style() // ui-library.card()所有模块的成员都自动挂在了ui-library这个命名空间下。用户不用关心内部拆了多少个文件,只需要记住一个入口。这就是@forward存在的意义:帮你构建“对外 API 的聚合层”。
4.2 转发时的成员控制:隐藏与添加前缀
@forward不只是无脑转发,它还提供了精细的成员控制能力。你要知道,一个库内部会有大量实现细节(比如私有变量、内部混合),这些东西如果全部暴露给用户,会导致 API 表面变得臃肿混乱。这时候就可以用show和hide来筛选转发的成员。
// 只转发指定的成员 @forward 'theme' show $primary-color, $font-size; // 转发除了某个成员以外的所有成员 @forward 'theme' hide $-private-color;hide特别适合配合私有成员使用——只要你内部成员都用下划线开头命名,那么hide $-private-color这种写法就可以少写;但如果你的库里有些成员是公开的但不想让下游直接使用(比如某个混合虽然是公开命名但仅供内部调用),用hide就能把这些成员挡在 API 之外。
@forward还支持给转发的成员统一添加前缀,这在一个库要聚合多个同名成员时非常有用。举个例子:
// index.scss @forward 'theme' as theme-*; @forward 'layout' as layout-*;这样_theme.scss里的$color转发后变成了theme-$color,_layout.scss里的$gap转发后变成了layout-$gap。添加前缀的好处是:多个模块之间即使有同名的成员,经过前缀处理后也能和平共处,下游用户不会产生混淆。
4.3 与 @use 配合使用:先转发再加载的真正意义
@forward最强大的组合拳是和@use一起使用。@forward只负责“对外转发”,但在库的内部实现里,文件之间可能需要直接引用彼此的成员,这时候就需要@use了。
// _buttons.scss @use 'theme'; // 内部直接加载 theme 模块 @forward 'theme'; // 同时把 theme 转发给下游 @mixin button { background: theme.$primary-color; border-radius: theme.$border-radius; }这样设计的好处是:_buttons.scss一方面在内部使用theme的变量来实现自己的逻辑,另一方面又通过@forward把theme模块继续转发给下游,让下游也能直接访问theme模块的公开成员。这就避免了用户既要@use 'ui-library'又要单独@use 'styles/variables'的窘境。
从职责分工的角度理解:@use是“我需要用它”,@forward是“别人可能需要它”。两者配合,可以让库内部的实现细节与外部 API 的组织方式彻底解耦。
4.4 一个完整的库开发示例
为了让你更直观地理解@forward的价值,我直接贴一段典型的库入口文件代码:
// ui-library/index.scss @forward 'variables'; @forward 'mixins'; @forward 'functions'; @forward 'components/button'; @forward 'components/card'; @forward 'components/modal';下游使用:
@use 'ui-library'; .primary-btn { @include ui-library.button; color: ui-library.$primary-color; border-radius: ui-library.$radius-md; }如果哪天你决定把components/button拆分为components/button/base和components/button/variants,你只需要在入口文件里修改转发路径,下游代码完全不用动。这种架构上的弹性和向后兼容能力,是@import时代完全不敢想的。
5. 实际项目中的选型与迁移:从老写法切到新写法
5.1 新项目直接用 @use,别给自己埋雷
如果你现在正在搭建一个新项目,尤其是 Vue3 + Vite 或者 React + Webpack 这类现代前端工程,样式部分的写法没有任何理由再使用@import。直接全部用@use就好。
以 Vue3 项目为例,你现在安装 SCSS 支持通常会这样做:
npm install -D sass然后在 Vue 单文件组件(SFC)的<style lang="scss">块里直接写:
<style lang="scss"> @use '@/styles/variables' as v; @use '@/styles/mixins' as m; .custom-box { background: v.$bg-color; @include m.flex-center; } </style>注意这里我用的是@别名指向src/styles目录,前提是你在vite.config.js里配置了别名:
import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import path from 'path'; export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, });如果你想把全局的变量自动注入到每个组件的样式中(这样就不用每个文件都写@use 'variables'了),可以通过 Vite 的css.preprocessorOptions.scss.additionalData配置来实现:
export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables" as *;`, }, }, }, });这里又回到了之前提到的as *。在additionalData里用as *是有意的,因为这里注入的是纯变量,并且你能够控制变量命名不冲突。但如果是用法相对复杂的混合或函数,我还是建议保持命名空间,避免在组件样式里出现“找不到这个混合是哪里来的”的困惑。
5.2 老项目迁移:用官方迁移工具 sass-migrator
对于已经在维护的老项目,手动把所有@import改成@use是不现实的事情——文件动辄几十上百个,个个都要改。好消息是,官方提供了自动化迁移工具sass-migrator,专门用来做这件事。
首先安装工具:
npm install -g sass-migrator然后在项目根目录执行:
sass-migrator module --migrate-deps main.scss--migrate-deps的意思是递归迁移所有被main.scss引入的依赖文件。工具会做以下几件事:
- 把所有
@import替换为@use; - 为所有被引入模块的成员自动加上命名空间前缀;
- 处理
@import和@use混用导致的顺序和冲突问题; - 把
@import 'foo'这种写法转换成@use 'foo'并调整成员调用方式。
迁移完成后,你需要手动检查几个地方:
一是检查是否生成了不必要的as *或命名空间别名,工具有时为了兼容会使用一些比较保守的策略,可能会多做一些处理,需要你手工优化。 二是检查有没有混合使用@use和@import的边界情况。Sass 官方规定,一个文件里不能同时用@import和@use引入同一个模块,这种场景工具不一定能完美处理,可能需要你手工拆分。 三是检查with配置。如果原来的@import文件里用变量覆盖的方式做配置(即在文件里直接给$xxx重新赋值),迁移工具不会自动转成@use ... with的写法,需要你手工改。
5.3 迁移后的常见编译报错与处理思路
迁移过程中最常见的报错有两个。
第一个报错是Error: This module and the new module both define a variable named "$xxx"。这个报错出现的场景是:迁移工具帮你把所有成员都加上了命名空间前缀,但某些旧代码里可能已经手动写过带前缀或完全不带前缀的引用方式,导致新旧代码混淆。解决办法是全局搜索这个变量名,把旧引用方式统一修正为命名空间.$xxx的新写法。
第二个报错是Module loop: this module is already being loaded。这个我们前面提到过,通常是同一个模块被多个文件用不同的with配置加载导致的。迁移后如果出现这个报错,请检查入口文件里是不是有多个@use 'xxx' with (...)的语句,或者不同文件里的@use语句配置冲突了。解决办法是把模块的配置集中到一个入口文件里完成,其他文件只做普通的@use,不带with。
5.4 Vue3 项目实践中如何处理全局样式与组件样式
在 Vue3 + SCSS 的项目里,我经手的实践通常是把全局样式拆成几个模块文件,然后通过一个入口文件统一转发:
// src/styles/index.scss @forward 'variables'; @forward 'mixins'; @forward 'transition'; @forward 'reset';然后在vite.config.js里,通过additionalData注入一个自动@use(不带命名空间的变量注入),同时在入口文件里再显式加载一次完整的样式表:
css: { preprocessorOptions: { scss: { additionalData: `@use "@/styles/variables" as *;`, }, }, }需要注意,这种“自动注入变量”的方式有一个副作用:如果某个组件本来就定义了同名变量,那么组件内的变量会覆盖全局变量,这是正常的(Vue 组件样式默认有 scoped 隔离,变量覆盖只发生在当前组件样式块内),不必担心全局污染。
真正要小心的反而是另一个场景:你在组件样式的<style lang="scss">里引用了某个混合(mixin),结果编译时候提示“找不到”,这通常是因为混合没有被注入到当前作用域,而你的additionalData只注入了变量模块,没有注入混合模块。解决办法有两个:要么在additionalData里把混合模块也注入进来(注意用命名空间防止冲突),要么在组件样式文件里显式写一行@use '@/styles/mixins' as m;再使用m.xxx。我建议采用显式@use的方式,因为自动注入混合会让组件样式代码的可读性变差——看到flex-center()不知道是在哪定义的。
6. 常见问题与排查技巧实录
6.1 问题速查表
我把日常开发和团队答疑时遇到的高频问题整理成了一张表,方便你直接对照排查:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
编译报错:@importis deprecated | Sass 版本过新,弃用警告已输出 | 尽快迁移到@use,或临时降级 Sass 版本(不推荐) |
| 编译报错:There is no module with the namespace "xxx" | 文件路径写错或文件未通过@forward转发 | 检查@use和@forward的路径是否正确 |
| 变量访问不到:Undefined variable | 变量是私有成员(下划线开头) | 打开被引入文件确认变量命名,或使用公开变量 |
| 同模块配置冲突:Module loop | 多个文件分别对同一模块执行了不同with配置 | 把配置集中到唯一的入口文件,其他文件不带with使用 |
| 组件样式里找不到混合 | additionalData只注入了变量,没注入混合模块 | 在组件样式里显式@use对应模块 |
| 命名空间冲突 | 两个模块as *后成员重名 | 取消as *,改用默认命名空间或自定义别名 |
使用@use后样式无法覆盖库内样式 | @use的模块成员带有命名空间且变量不可变,覆盖方式和@import完全不同 | 先确认库的设计是否提供配置入口(with+!default),否则尝试通过@forward show/hide二次封装库模块 |
| 迁移工具报错无法处理某些复杂文件 | 文件里有动态路径或混入@import语法 | 手工拆分:把动态路径部分拆成独立模块,再分别迁移 |
6.2 命名空间和路径解析的几个避坑点
@use的路径解析规则和@import类似,都是相对于当前文件所在目录来解析的。但有几个细节容易踩坑:
第一,@use在解析时会自动优先匹配以下划线开头的部分文件。比如你有两个文件_variables.scss和variables.scss,@use 'variables'优先匹配哪个?答案是_variables.scss。这是 Sass 文件命名的老约定:下划线开头的文件被视为“部分文件”,不会被单独编译成 CSS 文件,@use时也不用写下划线。
第二,@use的路径如果写的是相对于当前文件的位置,在 Vite 中结合别名使用时,最好统一用绝对别名路径,比如@/styles/variables,这样可以避免嵌套层级深的时候../写错导致的“module not found”。
第三,@use和@forward都不支持动态路径或变量路径,路径必须是编译期可静态解析的字符串字面量。如果你在代码里写@use $path,会直接报错。这也是为什么迁移工具对动态路径无能为力,只能靠手工处理。
6.3 我在实际项目中积累的几个技巧
第一个技巧关于@use和@forward的目录组织。我通常会在src/styles下建立一个abstracts目录,专门放变量、混合、函数等不产生实际 CSS 输出的文件;再建一个vendors目录放第三方样式库的二次封装;最后用一个index.scss作为整个样式系统的对外出口。这种方式让新同学上手项目时,只需要看index.scss就知道整个样式系统有哪些模块可以用,不用去翻目录树。
第二个技巧关于自动注入变量的边界。additionalData里用as *注入变量时,一定要只注入纯变量模块,不要混入会输出 CSS 的内容(比如 reset 样式)。因为如果注入的内容里包含实际的 CSS 规则,那么每个组件的样式都会重复输出一遍这些规则,造成严重的冗余。我见过有人把@forward 'reset'写进additionalData导致整个项目每个组件的编译产物里都带着 reset 样式,最终体积膨胀得很厉害。
第三个技巧关于调试映射。@use的一个隐藏优势是它天然支持更好的调试体验——因为它的模块关系是静态的,浏览器开发者工具中的 CSS 来源映射可以精确到“这个变量来自哪个文件的哪一行”,而@import由于是文本复制,来源映射经常错乱。遇到样式来源追踪困难时,改用@use之后一般都能快速定位,这个问题会自然消失。
第四个技巧是:当你对@use和@forward的某条语法不确定时,最快的验证方式是开一个临时目录,写一个几行的 demo,然后在命令行用sass命令编译看输出。不要直接在项目里试错,那样反馈链路太长。你可以这样操作:
mkdir sass-test && cd sass-test npm init -y npm install -D sass echo "@use 'theme'; body { color: theme.\$primary-color; }" > test.scss echo "\$primary-color: #f00;" > _theme.scss npx sass test.scss几秒钟就能看到编译结果,比在项目里瞎试高效得多。
6.4 避坑指南:从 @import 到 @use 的思维转变
最后想强调一个思维层面的转变。很多人迁移时只是机械地把@import替换成@use,然后在成员调用的地方加上命名空间前缀,这其实只完成了表面工作。真正的思维转变在于两点。
第一,要理解“显式优于隐式”。@import时代,所有成员都是全局可见的,看起来方便,代价是混乱;@use时代,每个文件用到的模块和成员都写得清清楚楚,代码读起来更累一点,但维护起来却轻松得多。团队协作时,这种显式性带来的确定性收益是非常大的——你不需要再像侦探一样猜测某个变量到底哪儿来的、会不会被其他文件覆盖。
第二,要用“模块设计”而非“文件拼接”的思路组织样式。@import时代我们把文件视为“要拼进整体的一块碎片”,所以文件之间可以有任意错综复杂的依赖关系;@use/@forward时代,我们应该把每个文件视为一个独立的模块,明确它的输入(通过@use引入的依赖)、输出(对外暴露的公开成员),以及它对外的配置接口(通过with和!default的方式)。有了模块边界,样式的可测试性、可复用性和可维护性都会上一个层级。
我在实际改造一个大型后台项目的样式系统时,刚开始也是简单地全局替换,结果编译爆出一堆错误。后来我停下来重新梳理了整个项目的样式架构,把几十个文件按照“abstracts(变量/混合/函数)— components(组件样式)— pages(页面样式)”三层结构重新组织,用@forward建立统一的对外出口,才算真正完成了迁移。这个过程的收获是:迁移不仅是替换关键词,更是重新思考样式架构的契机。
如果你现在正在做这件事,我的建议是先别急着全局替换。先找一个最基础的入口文件,比如根组件的样式或者全局样式入口,用@use重写一遍,编译通过后再逐步扩大迁移范围。每一步都保证项目是可运行的,遇到问题能很快定位到是哪个文件的问题。等你把整个项目都迁移完,再回头看,会发现样式系统的混乱程度已经大大降低了——这正是这三者之间区别的价值所在。