告别@import:Dart Sass 1.77.0模块化架构迁移实录

我们团队去年接手了一个运行了四年的电商后台管理系统,代码量大概在 12 万行左右。打开样式目录的时候,我整个人都愣住了:所有的 SCSS 文件都在 styles 目录下平铺,没有任何层级,而且几乎每一个文件的第一行都是 @import "variables";@import "mixins";

当时项目还在用 LibSass,编译一次全量样式平均需要 800ms。最要命的是,有一次我改了一个全局变量 $primary-color,结果因为 @import 的加载顺序问题,导致某个深层组件里的按钮颜色没变,而另一个弹窗的颜色却变了,排查了整整两个小时。

后来我们决定直接升级到 Dart Sass 1.77.0(2024年5月发布的最新稳定版),并彻底干掉 @import,全面迁移到 @use@forward

为什么一定要换?@import 最大的问题就是它没有命名空间。它就像是你把所有的工具都倒在一个大箱子里,变量、函数、混合宏全部全局暴露。如果你在 a.scss 里定义了一个 $color,在 b.scss 里也定义了一个 $color@import 之后谁覆盖谁全看运气。而 @use 则像是一个个贴好标签的工具盒,你用哪个拿哪个,互不干扰。

迁移的过程其实比想象中要有趣。我们第一步是把 variables.scss 改造成了模块。

这是改造前的 variables.scss

$primary-color: #1890ff; $success-color: #52c41a; $font-size-base: 14px;

改造后,我们在 styles/_index.scss 里统一管理出口:

// styles/_index.scss @forward 'variables'; @forward 'mixins'; @forward 'functions';

然后在具体的组件里,我们不再全局引入,而是按需加载:

// components/_button.scss @use '../styles' as *; // 或者 @use '../styles' as S; 然后用 S.$primary-color .btn-primary { background-color: variables.$primary-color; // 明确指定来源 font-size: variables.$font-size-base; }

这里有个细节。刚开始我们想偷懒,直接用 @use '...' as * 来屏蔽命名空间,后来发现这又回到了 @import 的老路上,失去了模块化的意义。于是我们强制规定,除了 _index.scss 这种聚合文件,其他文件必须带命名空间调用。

还有一个实际遇到的问题:旧代码里大量使用了 @import 来引入第三方库(比如之前的 normalize.css)。我在迁移时发现,Dart Sass 1.77.0 虽然还兼容 @import,但在编译日志里会疯狂报警告。为了彻底解决,我写了一个小脚本,把所有的 @import 'xxx' 替换成了 @use 'xxx',但对于那些不需要命名空间的纯 CSS 文件,我们保留了 @import,因为 @use 对于纯 CSS 文件来说有点杀鸡用牛刀。

迁移完成后,最直观的变化是编译速度。全量编译从 800ms 降到了 120ms 左右。因为 @use 机制下,同一个模块只会被加载一次,而 @import 每次都会重新解析。另外,样式体积也小了大概 5%,因为避免了重复加载带来的冗余代码。

如果你现在还在用 @import,我建议趁着项目还没大到无法维护,赶紧动手。Dart Sass 官方已经明确表态,@import 终将被移除,现在迁移是为了以后少掉头发。

实战复盘:某百万DAU社区Sass变量与主题切换架构

我之前负责过一个社区产品的重构,日活大概在 150 万左右。产品经理提了一个需求:不仅要支持亮色和暗色模式,还要允许用户自定义主题色。当时我们面临的选择是:用 CSS 变量还是 Sass 变量?

很多人会直接说用 CSS 变量,因为可以运行时动态切换。但我当时做了一个混合架构:编译时用 Sass 变量处理基础逻辑,运行时用 CSS 变量处理动态切换。

为什么不全用 CSS 变量?因为我们的组件库非常复杂,很多地方需要根据主题色计算出不同深浅的阴影、悬停色。如果全用 CSS 变量,在 Sass 编译阶段这些变量是死的,没法用 darken()lighten() 函数处理。

我是这么设计的。在 styles/theme/_variables.scss 里,我定义了 Sass 变量作为“源数据”:

// styles/theme/_variables.scss @use 'sass:map'; // 默认亮色主题配置 $light-theme: ( 'primary': #1890ff, 'bg-color': #ffffff, 'text-color': #333333, 'border-color': #d9d9d9 ); // 暗色主题配置 $dark-theme: ( 'primary': #177ddc, 'bg-color': #141414, 'text-color': #ffffff, 'border-color': #434343 );

然后,我写了一个 @mixin 来生成 CSS 变量。这个 @mixin 接收一个主题 Map,把它转换成 CSS 变量:

// styles/theme/_generator.scss @mixin generate-theme-vars($theme-map) { :root { @each $key, $value in $theme-map { --#{$key}: #{$value}; } } }

在入口文件里,我根据一个 Sass 变量 $current-theme 来决定生成哪个主题的 CSS 变量。但这还没完,因为用户可能随时切换暗色模式。

所以我在 styles/theme/_index.scss 里做了这样的事:

// styles/theme/_index.scss @use 'variables' as vars; @use 'generator' as gen; // 编译时生成默认主题(亮色) @include gen.generate-theme-vars(vars.$light-theme); // 为暗色模式预留选择器 @media (prefers-color-scheme: dark) { @include gen.generate-theme-vars(vars.$dark-theme); } // 手动切换暗色模式 .dark-mode { @include gen.generate-theme-vars(vars.$dark-theme); }

在组件里,我不再直接写颜色值,而是用 var() 去读取这些 CSS 变量:

// components/_card.scss @use '../styles/theme' as theme; .card { background-color: var(--bg-color); color: var(--text-color); border: 1px solid var(--border-color); transition: background-color 0.3s; &:hover { // 这里用到了 Sass 函数计算,因为 CSS 变量没法直接传进 Sass 函数 // 所以我们得在生成变量的时候就处理好 // 或者在这里用 CSS 的 calc 或者 rgba 处理 background-color: rgba(var(--primary-rgb), 0.1); } }

注意上面那个 hover 的处理。为了让 rgba 能工作,我在生成 CSS 变量的时候,还得把颜色拆成 RGB 格式:

// 改进 generator @mixin generate-theme-vars($theme-map) { :root { @each $key, $value in $theme-map { --#{$key}: #{$value}; // 如果是颜色值,额外生成一个 rgb 版本 @if type-of($value) == color { --#{$key}-rgb: #{red($value)}, #{green($value)}, #{blue($value)}; } } } }

这个架构上线后,表现非常稳。用户切换主题时,只是改变了根节点上的 CSS 变量,不需要重新加载样式表。而 Sass 在编译阶段帮我们处理了所有的逻辑运算和变量生成,兼顾了开发效率和运行性能。我们当时测过,切换主题的响应时间基本在 30ms 以内,用户完全感知不到卡顿。

性能对决:@mixin与@extend的编译体积压测与选型策略

很多刚接触 Sass 的朋友都会纠结:@mixin@extend 到底该用哪个?我在一次性能优化专项里,专门对这两个特性做了一次压测。

当时我们的项目里有一个 clearfix 样式,大概有 200 多个组件都在用。之前有个同事为了图省事,直接写了一个 @mixin clearfix,然后在每个组件里 @include

// 旧方案:使用 Mixin @mixin clearfix { &::after { content: ''; display: table; clear: both; } } .card { @include clearfix; } .modal { @include clearfix; } // ... 假设有 200 个这样的组件

我写了一个脚本,模拟了 200 个组件分别使用 @mixin@extend 的情况,然后对比编译后的 CSS 体积和编译耗时。

压测环境:Dart Sass 1.77.0,MacBook Pro M1,单文件编译。

方案一:Mixin(传参复用)

// test_mixin.scss @mixin clearfix { &::after { content: ''; display: table; clear: both; } } @for $i from 1 through 200 { .component-#{$i} { @include clearfix; } }

方案二:Extend(继承复用)

// test_extend.scss %clearfix { &::after { content: ''; display: table; clear: both; } } @for $i from 1 through 200 { .component-#{$i} { @extend %clearfix; } }

压测结果对比

| 特性 | 编译后 CSS 体积 | 编译耗时 | 生成代码特征 |

| :--- | :--- | :--- | :--- |

| @mixin | 约 12.5 KB | 45ms | 每个选择器都重复包含完整的 ::after 代码块 |

| @extend | 约 2.8 KB | 120ms | 所有选择器合并成一个逗号分隔的列表,共享一个 ::after 代码块 |

数据很直观。Mixin 的体积是 Extend 的 4 倍多。因为 Mixin 是“复制粘贴”逻辑,你 @include 一次,它就复制一份代码到那个位置。而 Extend 是“合并同类项”,它把用了 @extend 的选择器全部拎出来,放在一起写一次样式。

但是,编译耗时上 Extend 反而更慢。这是因为 Extend 需要分析整个文档的选择器关系,进行合并计算,这是一个相对耗时的过程。不过对于 200 个组件来说,多出来的 75ms 完全可以忽略不计,毕竟体积优化带来的网络传输收益更大。

那是不是应该无脑用 Extend?

并不是。我遇到过一个坑。有一次我想用 Extend 来继承一个 .btn 的样式,结果编译出来的 CSS 里,.btn 这个类名也被输出了。但在我的 HTML 里,我其实只想用 .btn-primary,并不想让 .btn 这个类出现在 DOM 里(虽然没影响,但看着不舒服)。

后来我学乖了,用占位符选择器 %。就像上面的压测代码里写的,用 %clearfix 而不是 .clearfix。这样编译后的 CSS 里就不会出现 .clearfix 这个多余的类。

我的选型策略是这样的

所以,回到那个 200 多个组件的 clearfix 问题,我最后的方案是把 Mixin 改成了 Extend,体积直接砍掉了 10KB 左右。对于首屏加载来说,这 10KB 的优化还是很有意义的。

4. 独家调试:解决大型项目Sass编译慢与HMR热更新瓶颈

去年我在重构一个电商后台管理系统时,遇到了一个非常头疼的问题。这个项目迭代了三年多,样式文件膨胀到了 1200 多个,SCSS 代码量超过 8 万行。随着业务逻辑越来越复杂,我每次修改一个按钮的样式,按下保存键后,浏览器就像死机了一样,HMR(热更新)的转圈动画至少要转 3 到 5 秒才能看到变化。有时候甚至直接触发了 Webpack 的重新全量编译,等待时间长达 15 秒。

作为开发者,这种等待是极其消磨耐心的。当时我查了一下环境,项目使用的是 Dart Sass 1.69.5,构建工具是 Vue CLI 5(基于 Webpack 5)。我决定彻底排查一下这个问题。

我做的第一件事是用 time 命令和 Node.js 的 performance API 测量了一次完整的编译耗时。结果发现,编译这 8 万行 SCSS 竟然需要 850ms。而 HMR 之所以慢,是因为每次修改都会触发整个 main.scss 的依赖树重新计算。

排查过程与根因分析

我打开项目的 main.scss,发现了典型的“意大利面条式”引用。我们使用了旧的 @import 方式,导致每一个文件都会重复引入 variables.scssmixins.scss

// 旧的 main.scss —— 性能杀手 @import 'variables'; @import 'mixins'; @import 'reset'; @import 'button'; // button里又@import了一次variables @import 'table'; // table里也@import了一次variables // ... 几百个文件

Dart Sass 1.77.0(2024年5月发布的最新版本)中,虽然编译速度已经比早期的 Ruby Sass 快了很多,但 @import 是一个没有命名空间、没有缓存机制(在旧逻辑下)的指令。它会导致变量和 Mixin 在内存中被反复加载和解析。

解决方案:迁移至 @use 与升级 Dart Sass

我制定了一个迁移方案。首先,我将 Dart Sass 升级到了 1.77.0,这是目前的官方主流实现,替代了已经停止维护的 LibSass。然后,我写了一个 Node.js 脚本,批量将 @import 替换为 @use

这是改造后的代码结构:

// abstracts/_index.scss (入口文件,使用 @forward 暴露) @forward 'variables'; @forward 'mixins'; @forward 'functions'; // components/_button.scss (组件文件,按需加载) @use '../abstracts' as *; // 只加载一次,且带有命名空间(这里用*简化,实际建议带命名空间) .btn-primary { background: abstracts.$primary-color; // 明确引用,避免冲突 @include abstracts.flex-center; }

为什么这么做?

@use 规则最大的不同在于,同一个模块在同一个编译上下文中只会被加载一次。无论你在 100 个文件里 @use 'variables',Sass 在编译时只会处理一次变量定义。这直接把我的编译时间从 850ms 降到了 120ms

关于 HMR 的进一步优化

即便编译快了,Webpack 的 HMR 依然可能因为依赖图过大而卡顿。我发现项目中很多 SCSS 文件里写了大量的 @each 循环来生成辅助类(Utility Classes)。

// 这是一个典型的性能瓶颈代码 $colors: (primary: #1890ff, success: #52c41a, warning: #faad14); @each $name, $color in $colors { .text-#{$name} { color: $color; } .bg-#{$name} { background-color: $color; } // 实际上可能有几十个属性,导致生成的CSS体积巨大 }

这种逻辑在开发模式下也会全量执行。我采取的策略是:开发环境动态生成,生产环境按需生成

我修改了 vue.config.js 的配置,利用 sass-loaderadditionalData 注入环境变量:

// vue.config.js 片段 module.exports = { css: { loaderOptions: { scss: { additionalData: ` @use "sass:meta"; $env: "#{meta.inspect("development")}"; @import "@/styles/config"; ` } } } }

然后在 SCSS 中做判断:

@use 'sass:meta'; @if $env == 'development' { // 开发时只生成常用的几个,减少编译负担 @each $name, $color in (primary: #1890ff, success: #52c41a) { .text-#{$name} { color: $color; } } } @else { // 生产时全量生成 @each $name, $color in $colors { .text-#{$name} { color: $color; } } }

经过这一轮优化,我们的 HMR 响应时间稳定在 200ms 以内,开发体验有了质的飞跃。这让我意识到,在大型项目中,Sass 不仅是写样式,更是在管理编译性能。

---

5. 混合架构:Sass逻辑层与Tailwind原子类的协同开发模式

最近社区里经常讨论一个问题:用了 Tailwind CSS 还需要 Sass 吗?甚至有激进的观点认为 Sass 已经过时了。作为经历过从写 CSS 到 Less,再到 Sass,现在又接触 Tailwind 的开发者,我在最近的一个 SaaS 后台项目中尝试了一种混合架构:用 Sass 作为逻辑层(Logic Layer),用 Tailwind 作为原子类层(Atomic Layer)

这种架构并不是简单的“混用”,而是有明确的分层逻辑。

为什么需要混合?

Tailwind 非常适合快速搭建 UI 骨架,比如间距、排版、颜色。但是,我在做那个 SaaS 项目时遇到了一个场景:我们需要实现一个复杂的“数据看板”,里面有 20 多种不同状态的卡片,每种卡片都有独特的渐变背景、阴影和动画。

如果纯用 Tailwind,我的 HTML 会变成这样:

<!-- 纯Tailwind的噩梦 --> <div class="bg-gradient-to-r from-blue-500 to-purple-600 shadow-lg rounded-xl p-6 text-white transform hover:-translate-y-1 transition-all duration-300 ease-in-out relative overflow-hidden ..."> <!-- 内容 --> </div>

这种代码不仅难以维护,而且一旦设计稿微调(比如把 purple-600 改成 indigo-600),我得全局替换,或者去改 tailwind.config.js

我的解决方案:Sass 负责“设计系统”,Tailwind 负责“快速排版”

我决定保留 Sass 的强项——逻辑抽象。我利用 Sass 的 Mixin 和 Functions 来定义那些复杂的、需要计算的状态样式。

具体做法是,我在 styles/ 目录下建立了一个 components/ 文件夹,专门用 SCSS 写“高阶组件样式”。

// styles/components/_status-card.scss @use 'sass:map'; @use '../abstracts/variables' as vars; @use '../abstracts/functions' as fn; // 定义一个混合宏,专门处理复杂的卡片状态 @mixin status-card($status) { $config: map.get(vars.$status-config, $status); // 这里用了Sass的逻辑判断,Tailwind很难做到这种动态逻辑 @if not $config { @error "Status #{$status} not found in config."; } background: linear-gradient(135deg, fn.get-color(map.get($config, 'start')), fn.get-color(map.get($config, 'end'))); box-shadow: 0 10px 30px -5px rgba(map.get($config, 'shadow'), 0.3); // 基于变量计算复杂的属性 border-left: 4px solid fn.get-color(map.get($config, 'accent')); .card-icon { opacity: 0.8; transform: scale(1.2) rotate(#{map.get($config, 'rotate')}deg); } } // 在页面中调用 .dashboard-card--success { @include status-card('success'); }

如何在 HTML 中协同?

在 HTML 层面,我不再写一大堆 Tailwind 类来定义样式,而是把 Tailwind 用作“布局工具”,把 Sass 定义的类用作“皮肤”。

<!-- 混合架构下的代码 --> <div class="dashboard-card--success grid grid-cols-3 gap-4 p-4 rounded-xl"> <!-- 这里的 grid, gap, p-4, rounded-xl 是Tailwind负责布局和间距 --> <!-- 而 dashboard-card--success 是Sass负责的颜色、渐变、阴影等复杂逻辑 --> <div class="card-icon col-span-1">...</div> <div class="col-span-2">...</div> </div>

这种架构的优势在哪里?

// abstracts/_variables.scss $status-config: ( 'success': (start: blue-500, end: indigo-600, shadow: blue-800, rotate: 0), 'danger': (start: red-500, end: pink-600, shadow: red-900, rotate: 15), // 改这里就行 );

这种“Sass 做逻辑,Tailwind 做布局”的模式,特别适合那种设计规范复杂、状态多、且需要长期维护的企业级项目。它不是为了炫技,而是为了在开发效率和代码可维护性之间找一个平衡点。如果你正在纠结选 Tailwind 还是 Sass,不妨试试这种“双剑合璧”的方案。

站长实战手记

一次差点让我通宵的样式重构

去年我接手了一个跑了四年的老项目,是一个日活过百万的社区。接手时我头都大了,Sass 文件嵌套深得像迷宫,一个按钮的样式要穿透七八层才能找到源头。最要命的是,他们还在用 @import,每次改一个变量,整个项目几百个文件全部重新编译,HMR 热更新能卡十几秒,改个颜色我都能去接杯水回来。

我当时第一反应就是全量迁移到 @use@forward。但真动起手来才发现,老代码里到处是全局变量和函数的隐式依赖。我花了整整一个周末,一边跑着编译脚本看依赖树,一边手动拆解模块。最后我做了一个决定:不追求一步到位。我把核心的主题变量(颜色、字体)先抽离成独立的 theme 模块,业务组件暂时保留旧的引用方式,通过一层适配文件慢慢过渡。

折腾完之后,编译速度从原来的 8 秒降到了 2 秒以内,开发体验直接起飞。

我的真实看法

关于 Sass,我有几个不吐不快的实话:

* 别迷信 @extend:很多人喜欢用 %placeholder 来复用样式,觉得生成的 CSS 体积小。但在大型项目里,这玩意儿极其容易导致选择器无限制膨胀。我在压测里发现,超过三层继承,生成的 CSS 体积反而比 @mixin 大。我现在基本只用 @mixin,除非是那种极其简单的基类。

* 避免过度嵌套:Sass 给的嵌套能力太爽了,但千万别超过三层。我见过有人写 .header .nav .list .item .link,这种代码维护起来就是灾难。

* 什么时候别用:如果你只是做个简单的官网或者活动页,真没必要上 Sass。直接写原生 CSS 配合 CSS Variables,反而更轻量,构建配置也省心。

给读者的建议

学 Sass 别光看文档里的语法糖,多去看看别人怎么组织文件结构的。很多时候,决定你代码质量的不是你会不会写 @function,而是你能不能把 _variables.scss 设计得让人一眼看懂。遇到编译慢了,先别怪工具,看看是不是自己的嵌套层级或者模块引用乱了套。