一文詳解sass新特性——模塊

1. 簡介

2. @import的缺點

3. 模塊化的核心@use

3.1 @use的基本用法

3.2 @use@import的區別

3.3 配置樣式庫的基礎變量

4. 給庫開發者使用的利器@forward

4.1 @forward的基本用法

4.2 用show/hide控制成員是否可見

4.3 給不同的子模塊添加前綴

5. sass內置模塊

6. 兼容性

7. 遷移指南

8. 參考資料

簡介

2019年十月一號,sass團隊推出了sass的模塊化機制,通過新關鍵詞@use@forward,變量、mixin和函數從此擁有了命名空間。並且,sass對已有的內置函數進行了歸類和整理,分類到了各個內置模塊下。

引入模塊化機制,讓sass向更成熟的階段邁進了很大的一步。目前sass的各個實現中,僅Dart Sass 1.23.0完全支持這些新特性。sass團隊宣稱兩到三年後纔會完全廢棄@import等舊語法,現在處於新舊語法共存的過渡時期。

compatibility

sass的模塊化機制顯然是一個major的提升,那麼,如此大費工夫,它能夠解決什麼痛點呢?讓我們先來看看之前的@import所帶來的問題吧!

@import的缺點

@import主要有以下5個缺點:

  1. 無法知道變量、mixin、函數具體是在哪裏定義的。比如說,a.scss文件中定義了變量heightb.scssac.scssbcheight,b.scss文件中引入了a,c.scss文件又引入了b,那麼在c文件中,height是可用的,但無法確定其來源。

  2. 嵌套import會導致重複的css代碼,還可能引發奇怪的副作用。設想這樣一個場景,一個頁面中動態引入了一個組件,頁面本身需要加載page.css,組件的樣式由component.css決定,而這兩個樣式表的源scss文件中都用到了common.scss,那麼在動態引入組件的時候,common.css中的樣式就會被重複加載,可能對原有的樣式造成覆蓋。

  3. 因爲沒有命名空間,css中的選擇器又天然是全局的,爲了避免撞名,不敢使用簡寫的class name,因此起名總是非常冗長。

  4. 沒有私有函數的概念。庫作者無法確保他們的私有工具函數不會被使用者直接獲取,直接使用私有函數可能導致混淆和向後兼容的問題。

  5. @extend規則可能會影響到樣式中的一切選擇器,而不是僅僅是作者所希望的那些。(對@extend不熟悉的童鞋可以看看這篇文章the-benefits-of-inheritance-via-extend-in-sass來具體瞭解下其使用場景和優缺點。)這一點會在下一節詳細論述。

天天寫@import,沒想到它存在的問題還真不少。那麼,sass又推出了什麼樣的功能,能夠替代@import呢?下面,我們來詳細解剖一下sass的模塊化機制。首先,就從@import的替代者———— @use說起。

@use, 模塊系統的核心

@use的基本用法

@use將取代@import,使css,變量,mixin, 函數都可以在不同的樣式表中複用。一個樣式表文件就是一個模塊,其命名空間會基於文件名自動生成,也可以進行自定義命名。變量、mixin和函數會默認在該命名空間下使用。見下例:

@use "bootstrap" as b;

.element {
  background-color: b.$body-bg;
  @include b.float-left;
}

如果使用了as *,那麼這一模塊就處於全局命名空間,可以直接使用其中的變量和mixin。但注意,如果多個模塊暴露出的變量命名重複,並且都使用了as *,那麼sass會報錯[Error: This mixin is available from multiple global modules:]。

@use "bootstrap" as *;

.element {
  @include float-left;
}

@use@import的區別

@use@import的區別在於:

  1. 不管使用了多少次樣式表,@use都只會引入和執行一次。
  2. 與全局使用相反,@use是有命名空間的,而且只在當前樣式表中生效。
  3. 或者_開頭的命名空間被認爲是私有的,不會被引入其他樣式表。
/* buttons.scss */
$_height: 20px;

/* tryuse.scss */
@use 'buttons';

.my-buttoon {
  background-color: buttons.$color;
  height: buttons.$_height;
}

私有命名空間

  1. 如果一個樣式表A包括@extend,那麼該擴展的作用域僅包括它本身和被它引入的上游模塊中的樣式規則,而不是引入了樣式表A的下游模塊。如下例:
// _upstream.scss
.a {@extend .b};
.b {c: d};

// downstream.scss
@use "upstream";
.b {x: y};

// css結果
.b, .a {
  c: d;
}

.b {
  x: y;
}

// 如果採用@import,則
.b, .a {
  c: d;
}

.b, .a {
  x: y;
}

這是很容易理解的,@import約等於直接把上游模塊的代碼粘貼過來,那麼在下游文件中對.b的任何css都會被應用於.a選擇器中。而@use是模塊化的,下游文件中的修改不會影響到上游文件。

注意,具有命名空間的成員只包括變量、mixin和函數。被@extend擴展的選擇器是不具有命名空間的,佔位符選擇器也沒有命名空間。使用了@use之後,佔位符選擇器的表現與普通選擇器一樣,下游模塊的代碼都不會污染使用了@extend的選擇器。

由於@extend仍然不具備命名空間,所有上游模塊的代碼均可以影響到全局,而且很難分清到底哪個擴展樣式是來自於哪個樣式文件。因此,筆者認爲仍需要謹慎使用@extend,可以更多地利用css層疊繼承樣式來完成@extend的功能。

配置樣式庫的基礎變量

@import的時代,很多樣式庫都需要用戶配置全局變量來覆蓋用!default定義的默認變量。在改爲使用@use之後,變量不再是全局的了,因此配置變量可以使用更有針對性的with語句。

// bootstrap.scss
$paragraph-margin-bottom: 1rem !default;

p {
  margin-top: 0;
  margin-bottom: $paragraph-margin-bottom;
}

// my-style.scss
@use 'bootstrap' with ($paragraph-margin-bottom: 1.5rem)

這樣,在my-style.scss文件中,被引用的bootstrap.scss文件裏$paragraph-margin-bottom的值就被設成了1.5rem。with語句只允許設置被引入模塊中已經被定義的默認變量(即使用了!default的變量),這樣還可以保護用戶免於輸入錯誤。

但要注意的是,一個模塊只能被配置一次,那就是在第一次引入的時候。 sass中的引入順序很重要,最好在入口文件中按順序引入需要的模塊,這樣前一個被引入的模塊會在下面的模塊被引入之前編譯好配置項。

給庫開發者使用的利器@forward

@forward的基本用法

@forward語句可以引入另一個模塊的所有變量、mixins和函數,將它們直接作爲當前模塊的API暴露出去,而不會真的在當前模塊增加代碼。這樣,庫作者可以更好地在不同源文件之間拆分代碼。不同於@use@forward不會給變量添加命名空間。

/* bootstrap.scss */
@forward "functions";
@forward "variables";
@forward "mixins";

注意,此時生成的bootstrap.css文件中,是不包含"functions"、“variables”、"mixins"代碼的,也不能直接在bootstrap.scss文件中使用這些模塊。而是需要在另一個文件中@import或者@usebootstrap模塊,再去使用這些方法。bootstrap.scss文件類似於一個傳輸中轉站,把上下游的成員變量無縫連接起來。

注意,直接寫在上游模塊的樣式仍然會被@forward進來。見下例:

/* upstream.scss */
...
footer {
  height: pow(2, 3) * 1px;
  font-weight: map.get($font-weights, "medium");
}

/* downstream.scss */
@forward "upstream.scss";

/* 生成的downstream.css */
footer {
  height: 8px;
  font-weight: 500;
}

show/hide控制成員是否可見

@forward "functions" show color-yiq;
@forward "functions" hide assert-ascending;

通過控制showhide,可以決定模塊中的哪些成員對引入後的模板可見。對隱藏的變量,在下游文件中不可以使用,相當於模塊私有成員。

給不同的子模塊添加前綴

大多數情況下,一個樣式庫會存在一個入口文件index.scss,然後在index.scss中引入其他的子文件。這種結構類似於多合一模塊。那麼,如果要在某一文件中@forward多個子模塊,就可以使用as子句,來使子模塊下的成員自動帶上前綴以區分。

/* material/_index.scss */
@forward "theme" as theme-*;
@forward "func" as func-*;

/* downstream.scss */

@use 'material' as *;

p {
  color: $theme-white;
}
/* 或者,命名空間從父到子 */

@use 'material';

p {
  height: material.func-pow(4, 3) * 1px;
  color: material.$theme-white;
}

/* 也可以在多合一模塊中爲theme相關的變量重新定義值 */
@use "material" with ($theme-primary: blue);

/* 這等價於: */
@use "material/theme" with ($primary: blue);

sass內置模塊

sass引入模塊化後,還有一個很大的改變,就是把原來的內置函數歸類到了內置模塊中。這些內置模塊包括:
sass:math, sass:color, sass:string, sass:list, sass:map, sass:selector, and sass:meta。使用時需要先@use引用內置模塊,才能使用模塊中的方法。

@use "sass:color";

.button {
  $primary-color: #6b717f;
  color: $primary-color;
  border: 1px solid color.scale($primary-color, $lightness: 20%);
}

代碼會被編譯爲:

.button {
  color: #6b717f;
  border: 1px solid #878d9a;
}

這些模塊被引入的時候需要加上命名空間,這樣可以大大避免sass內置函數與用戶自定義函數及css原生方法的命名衝突。此後,sass新增函數也會更加方便。

某些函數在新的內置模塊中的命名不同於原先作爲全局函數的命名。首先,已經擁有了手動命名空間的內置函數已經被移除了前綴。比如map-get()、change-color()等,現在重命名爲map.get(),color.change()。此外,一些容易被混淆的函數名稱也被重新命名了,比如unitless()現在改爲了math.is-unitless()comparable()改爲了math.compatible()

color相關的函數,如lighten(), darken(), saturate(), desaturate(), opacify(), fade-in(), transparentize(), 和fade-out()的行爲都非常反直覺,它們對屬性的增減是按照靜態比例的。因此,這些方法都從新的內置color模塊中移除了,取而代之的則是color.adjust()函數和color.scale()函數。

以下是color.adjust()color.scale()的函數簽名:

color.adjust($color,
$red: null, $green: null, $blue: null,
$hue: null, $saturation: null, $lightness: null,
$alpha: null)

color.scale($color,
$red: null, $green: null, $blue: null,
$hue: null, $saturation: null, $lightness: null,
$alpha: null)

*二者的區別在於color.adjust()是以固定值來改變顏色的屬性,而color.scale()是動態縮放的。*對color.adjust(),$red, green,green, 和blue需要在-255和255之間取值,huedeghue的值要麼以`deg`爲單位,要麼無單位,saturation和lightness100lightness需要在-100%和100%之間取值,alpha需要在-1和1之間取值。對color.scale(),取值均需在-100%到100%之間。

下面是一個使用color.adjust()的例子:

@use "sass:color";

$c: rgba(144, 233, 12);

p:first-child {
  background: color.adjust($c, $green: 150);
  /* 生成的顏色爲rgb(144, 255, 12),注意生成的rgb中各項取值在0-255之間*/
}

p:nth-child(2) {
  background: $c;
}

內置模塊meta中有一個新增的內置mixin,是meta.load-css($url,$with:())。 該mixin可以把urlcssurl中css樣式全部包含進來。*注意,url中的函數,變量和mixin在meta.load-css()後的scss中並不可用*。 @meta.load-css類似於@use,可以取代嵌套的@import,但只會返回編譯後的css代碼,還可以在代碼中動態使用。

/* index.scss */
@use 'sass:meta';

@function geturl($theme) {
  @return $theme + '.scss';
}

@each $theme in ('dark', 'light') {
  [data-theme='#{$theme}'] {
    @include meta.load-css(geturl($theme));
  }
}

/* _dark.scss */
$base-color:#000 !default;

p {
  background: $base-color;
  color: #fff;
}

meta.load-css()的第二個參數可以接受一系列配置項,如:

// 在加載之前配置'theme/dark'文件中的$base-color變量
@include meta.load-css(
  'theme/dark', 
  $with: ('base-color': rebeccapurple)
);

注意,load-css中的url不是真的網絡地址,而是一個scss文件的相對路徑或者絕對路徑,就像傳給@use的參數一樣

兼容性

sass生態系統不會在一夜之間就遷移到@use,所以在當下它會和@import共存一段時間。它們會以這樣的方式兼容:

  • 如果一個文件中包含@import,它本身又被別的文件以@use的方式引用,那麼這個文件的全局命名空間下的所有內容構成一個獨立的模塊。這個模塊的成員使用該文件的命名空間,被正常引用。

  • 如果一個文件包含@use,它本身又被別的文件以@import的方式引用,那麼所有暴露在該文件公共API上的東西都被會添加到樣式表的全局空間中。*注意,該文件私有API並不會自動暴露出來,即以@use被引入的上游文件中的API。*這樣,一個庫可以給導出的東西特定命名,即使對於@import而不是@use這個庫的用戶,特定命名也生效。

爲了使庫作者能在擁有獨立命名空間的基礎上,保持他們現存的@import向的API,新增了對只可import文件的支持。如果一個文件file被命名爲file.import.sass,那麼它只可被@import使用,並且只需寫@import "file"

sass團隊將會讓@use@import在未來一段很長的時間內共存,來幫助整個生態流暢地完成遷移。當然,爲了簡潔、性能和css兼容性,團隊最終的目標是完全拋棄掉@import。時間線具體如下:

  • Dart SassLibSass都支持模塊系統的一年後,或Dart Sass支持模塊系統的兩年後(以較早者爲準,最晚於2021年10月1日),將降級廢棄(deprecate)@import以及全局的核心函數,這些核心函數將只能通過sass內置的模塊來調用。

  • 該棄用生效一年後(最晚於2022年10月1日),我們將完全放棄對@import和大多數全局函數的支持。到時會發佈一個大版本的更新。

簡而言之,對@import的兼容還會支持至少兩年,實際上可能接近三年。

遷移指南

隨着新模塊系統的發佈,sass自動遷移工具也發佈了出來。它可以幫助你輕鬆地將大多數樣式表遷移到最新版本。安裝後,只需在應用內執行如下命令即可

sass-migrator module --migrate-deps <path/to/style.scss>

--migrate-deps標誌是指需不需要一併遷移那些被引用進來的上游模塊。遷移器將自動選取通過webpacknode_modules語法導入的文件,但也可以使用--load-paths標誌傳遞顯式的加載路徑。

如果想知道將要進行的更改而不實際進行更改,可以同時傳遞--dry-run標誌和--verbose標誌,以使遷移器僅打印出將進行的更改,而不會將其保存到磁盤。

如果想要對一個sass倉庫進行遷移,可以執行:

sass-migrator module --migrate-deps --forward=all <path/to/index.scss>

--forward標誌告訴遷移器添加@forward規則,以便用戶仍然可以通過@use庫名來加載庫中所有的mixins,變量和函數。如果給庫添加了手動命名空間(爲了避免名稱衝突),則可以通過傳遞--remove-prefix標誌刪除它。 甚至可以通過傳遞--forward=prefixed來僅僅@forward具有前綴的模塊成員。

sass並不是孤立的。在實際項目中,往往經由sass-loader轉換,與其他loader一起配合webpack打包。接下來,就讓我們看看如何在項目中進行sass的遷移。

sass有三種實現:Dart Sass,libsass,和目前已不再支持的Ruby Sass。

  • Dart Sass,用Dart語言寫的sass實現,於2016年11月1日發佈alpha版本。版本1.23.0之後完全支持模塊化機制。
  • libsass,用c/c++實現的sass,使用非常廣泛。node-sass是綁定了libsass的nodejs庫,可以極快的將.scss 文件編譯爲.css文件。
  • Ruby Sass,是最初的Sass實現,但是2019年3月26日被停止了,以後也不會再支持,使用者需要遷移到別的實現上。

從實現上看,顯然Dart Sass一騎絕塵,率先支持了模塊化機制。相信它將會憑藉優異的性能和快速的開發速度,成爲sass用戶的新寵。

要在webpack中使用Dart Sass也很簡單,只需爲sass-loader使用恰當的實現即可。根據sass-loader的文檔,只需要在項目中同時安裝sass-loadersass(即Dart Sass實現,沒錯,人家的命名就是這麼霸氣,就叫sass!)即可。

// package.json
{
  "devDependencies": {
    "sass-loader": "^7.2.0",
    "sass": "^1.22.10"
  }
}

如果同時安裝了node-sasssass,那麼sass-loader會默認使用node-sass,可以通過implementation選項來改變。

{
  test: /\.s[ac]ss$/i,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'sass-loader',
      options: {
        // 優先選用`dart-sass`
        implementation: require('sass'),
      },
    },
  ]
}

這樣,就可以成功地在項目中使用sass模塊語法了。筆者在一個vue項目中親測是可以的,所用到的webpack版本是4.41.2,sass-lodaer版本8.0.0,sass版本爲1.23.7。如果在升級sass後遇到this.getResolve is not a function的報錯,可以考慮升級webpack版本試試。

參考資料

  1. sass官網
  2. sass官宣: the-module-system-is-launched
  3. introducing-sass-modules
  4. the-benefits-of-inheritance-via-extend-in-sass
  5. sass佔位符文檔
  6. 爲什麼要避免使用extend
  7. sass-loader官方文檔
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章