如何從 SASS 無痛轉移到 POSTCSS

先說結論吧...目前這是不可能的(被歐),先別急著左鍵跳出啊,接下來要聊一下:為什麼要轉,轉得時候遇到什麼樣的問題,以及最後為什麼放棄了?是一個實作失敗的全紀錄。

先談談 SASS

SASS 在官網上形容自己是 most powerful CSS extension language,表示 sass 是基本 css 語法的延伸,提供很多更方便的寫法來撰寫你需要的樣式。他的作法是這樣:

" 先用 sass 定義的語法完成你的樣式表 .scss,接著再使用 sass 的 preprocessor,把檔案轉成一般的 .css 檔案。(通常透過 task runner,如 grunt、gulp) "

preprocessor 在做什麼?

以下幾個例子是目前 css 做不到的,所以需要 css preprocessor 的地方。

  1. 定義變數
    將重複用到的值,定義變數會比較方便處理,你不會想要輸入 :%s/OLD_VALUE/NEW_VALUE/gc 來代換樣式吧?

  2. 定義 mixin
    有些樣式,是多種 css 屬性設定集合起來的,這種情況就很適合用 mixin。另外也可以用在 vendor prefix 的需求。

  3. 載入其他樣式表
    上面所定義的包括像變數、mixin 等等必須要全部先 require 進來到同一個檔案才能夠使用。另外這樣也方便管理所有會使用到的 dependency。

  4. 運算子
    提供像 width: $compoment-width / 2; 這種運算變數的功能

另外還有幾個像 NestedPartialInheritance 功能,sass 的文件說明的相當詳細

為何要轉?

個人都是用 SASS 的語法寫樣式,所以相當習慣它的寫法。再加上 bootstrap v4 是使用 SASS。我在開發用戶端時,在樣式的部份,作法都是這樣:

" 一個 scss 檔上面列出所有需要的 bootstrap 樣式檔,然後在加上自己的樣式。"

/* ./bootstrap/ 有 boostrap v4 的 source */
/* import bootstrap 的核心檔 */
@import './bootstrap/_normalize.scss';
...

/* import 需要的 bootstrap component */
@import './bootstrap/_navbar.scss';
...

/* import 自己的樣式檔 */
@import './bootstrap/_my-style.scss';
...

一直以來這樣的作法使用 SASS 是沒有什麼問題的,但 SASS 有幾個問題:

  • 需要 Ruby
  • 無法決定要對樣式原始檔作哪些事,SASS 會作全部的 patch

這時候 postcss 出現了。

postcss 的原則

首先 postcss 是用 javascript 所寫的,所以就不用為了只要轉換 sass 而安裝 Ruby。

還有它的作法和 webpack 及 karma 很相似,就是**「需要作哪些事,再使用對應的 plugin」**。這樣的特性,第一是不會有冗贅的 dependency 和動作,所以質輕,而且速度快。第二,因為自己需要知道要用哪些 plugin,所以對自己樣式的寫法會更了解。

好,接下來開始嘗試用 postcss 來轉換 sass。

sass 2 postcss

以 grunt 舉例,下面是 GruntFile 裡的設定:

postcss: {
  default: {
    options:{
      processors: [
        // 列出 processor
      ],
      parser: require('postcss-scss') // 使用的 parser
    },
    src: 'app.scss',
    dest: 'app.out.css'
  }
}

首先,因為要轉換 sass,所以使用 postcss-scss plugin,並指定給 options.parser

照理說 postcss-scss 的目的是要正常的 parse sass 語法,並讓其他 processor 使用,但我使用的結果,它只有讓 sass 的註解語法生效:

// 這是 sass 的 comment
// parse 過後
/* 這是 sass 的 comment */

所以只好嘗試加上其他的 plugin 來完成剩下的部份。

因為 app.scss 中有列出所有的 dependency,所以我首先需要讓 @import 作用,postcss-partial-import plugin 用起來沒問題(不知道什麼原因用 postcss-import 來 import 的話,sass 不會先被 parse...)

import ok 的話,接下來就用 postcss-sassy-mixin 完美的處理掉 mixin 的語法和其中的變數代換,最後再使用 postcss-simple-vars 把檔案中的變數代換成值。

postcss: {
  default: {
    options:{
      processors: [
        // 列出 processor
        require('postcss-partial-import')(),
        require('postcss-sassy-mixins')(),
        require('postcss-simple-vars')()
      ],
      parser: require('postcss-scss') // 使用的 parser
    },
    src: 'app.scss',
    dest: 'app.out.css'
  }
}

目前看來還行

但接下來當在處理運算子的部份時,就找不到現成的 plugin 可以使用了...更何況還有 NestedPartialInheritance 這幾個語法還沒處理到。

所以我決定放棄了...

截長補短

是的,我放棄了,是放棄完全用 postcss 取代掉 SASS,但還是可以把 SASS 沒有的功能,使用 postcss 來完成啊!

這是妥協後決定的方案,先一部分的流程開始使用 postcss 來轉換樣式檔,既然 postcss 著重在「彈性」這項特性,未來如果 css 圈子有什麼變化,應該也可以很快代換才是。

所以現在 build 的流程,會先用 SASS 產生合法的 css,最後再用 postcss 的 autoprefixercssnano 兩個 plugin 來做到 vendor prefix 及 minify。

不斷更迭的 css 語法

為什麼 postcss 要轉換 sass 會這麼困難呢...?首先跟 SASS 的角色有關,不只 SASS,還有 LESS 都是之前比較知名的 preprocessor,他們的語法,其實都是自己訂的,並不是標準規範。

從 2015 年,W3C 開始著手規劃新一代的 css 標準,會加入許多 preprocessor 所提供的功能,在 cssnext 這個網站上有列出大致的走向

另外其實 postcss 有所謂的「sassy」plugin,比如 precss,稱為 sassy 就表示語法只是很像 SASS,但並不支援 SASS 的語法。

為什麼不和 SASS 統一呢?可能的原因很多,並不清楚。有可能是背後有其他政治因素(?),抑或者是 sassy 的語法是改良後的 SASS,被人認為是比較易讀、好懂的?舉兩者在使用 mixin 的差別:

/* SASS 定義 mixin */
@mixin icon ($name) {
  padding-left: 16px;

  &::after {
    content: "";
    background-url: url(/icons/$(name).png);
  }
}

/* 使用 mixin */
div {
  @include icon('jcc');
}

/* 'sassy' 定義 mixin */
@define-mixin icon $name {
    padding-left: 16px;

    &::after {
        content: "";
        background-url: url(/icons/$(name).png);
    }
}

/* 使用 mixin */
div {
  @mixin icon jcc;
}

兩者非常相像,但有決定性的不同,孰優孰劣見人見智(個人覺得 sassy 比較易讀一些。)

如果不統一的理由是後者的話,說不定哪天 SASS 會把語法更新成所謂的 sassy 方式。如果真的這樣,就完全能夠「無痛」的從 SASS 轉移到 postcss 了!