Sass を試す

2018年1月17日 3 開発 , ,

年末に PostCSS + cssnext と CSS Modules を試したのだが、コメ欄の指摘とそれを受けた追記を読んでもらえるとわかるとおり次世代 CSS 仕様の先取り = CSS.next のつもりで導入した cssnext が微妙だと感じはじめている。

これらの記事でも言及しているとおり cssnext の採用する PostCSS プラグイン は CSS Working Group の認知していなかったり取り下げられそうなものもある。前者は直せばよいけど後者については直近の仕様だと冗長な記述が避けられないことを意味している。私は少なくとも

  • モジュール管理
    • @import が URL ではなくモジュール参照として解決される
  • 変数 (定数) と Mix-In
    • 色やサイズなどを変数 (定数) として定義して共通化できる
    • Mix-In で拡張性のある共通プロパティーのセットを定義できる
  • ネスト
    • 親子関係にあるクラスで子が複数となるとき、ネストとして定義できる
    • 元が .parent .child1 {} .parent .child2 {} なら
    • .panret { child1 {} child2 {} } とできる

を必要としているのだけどモジュール管理は Bundler の範疇だし Mix-In とネストは採用されないだろうしで、CSS.next としては変数しか恩恵にあやかれない。ならば cssnext に拘る必要はない。また AltCSS としてみた場合、PostCSS のプラガブルな思想はエコ システムが多段になり複雑さを増すだけである。よって Stylus や Sass を採用するほうがよいだろう。

今までは Stylus を採用してきたが、これは PostCSS + cssnext 移行のきっかけとなったとおり開発が停滞している。ならばもう一つの雄である Sass を検討してみたい。というわけで Sass 単体と webpack による CSS Modules を試す。

Sass、SASS、SCSS

SASS (Syntactically Awesome StyleSheets) はツールと言語の側面がある。Web に散見される記事をみるに Sass と表記されたときはプロダクトやツール、SASS なら記法を指すようだ。そして Sass には更に SCSS という記法もある。このあたりの違いは以下の記事がわかりやすい。

ツールとしての Sass には Ruby と Node 版がある。開発環境として依存の少ないほうが好ましいため、Ruby 不要の Node 版を選ぶことにした。記法は SCSS を採用。SCSS は SASS よりも CSS に寄せているため、将来 Sass をやめたくなったときの負担が少なくて済むだろう。この点について Stylus でコロンとセミコロン省略していたことを後悔している。

SCSS の記法は Stylus と似ている。CSS 標準の記法をそのまま取り込めて拡張したスーパー セット的なものだから、拡張部分に注意すれば普通の CSS 感覚で書ける。機能的に大きく異るのは変数ぐらいだろうか。Stylus には透過的変数という概念があって

white = #ff0000;

.page {
  background-color: white;
}

のように CSS 標準定数を上書き可能で、この例だと white なのに赤としている。SCSS は

$white: #ff0000;

.page {
  background-color: $white;
}

のように変数に接頭辞 $ が必須となるため透過的に上書きすることはできない。抽象化としては機能が落ちるけれど、CSS 標準定数を破壊することがないと保証されているともいえる。一方で独自記法を持ち込むことになるから SCSS を捨てる際の枷になるかもしれない。

モジュール機能については拡張子によって @import の扱いが異なる。*.scss なら Stylus と同様に参照かつ結合対象。しかし *.css の場合は CSS 標準の URL 参照として扱われる。そのため npm としてインストールした normalize.cssnode_modules 配下から参照して結合、といった技が使えない。これを解決するためには normalize.scss にするか node-sass-package-importer という node-sass フォークがあるけど、いずれも標準じゃないので採用したくない。

これについて現在は公式資料にて

Notice we’re using @import 'reset'; in the base.scss file. When you import a file you don’t need to include the file extension .scss. Sass is smart and will figure it out for you. When you generate the CSS you’ll get:

と定義されている。ファイルとして取り込みたいなら拡張子を省略すればよい。これについては実際に

@import "../../node_modules/normalize.css/normalize";
@import "../../node_modules/normalize.css/normalize.css";
@import "./Sample";
@import "./Sample.css";

を試した結果、拡張子を省略したものはファイル結合される。.css まで記述すると @import url(path) に変換されることを確認した。なお .scss はどちらでもファイル結合される。つまり外部 CSS のファイル結合はフォークや SCSS 版を持ち出すことなく Sass 標準で対応可能。

面倒なら拡張子を省略して Sass の自動判定に任せるのがよいだろう。@import url(path) はパフォーマンス問題があるため通常は避けるものだから、特に困らないはず。ただし CSS Modules で同名 JavaScript と併置したくなった時などを考慮すると内部ファイルは拡張子まで記述したい。私は ES Modules でも npm など外部由来はモジュール名、内部は拡張子まで記述するようにしている。このあたりは好みがわかれそう。

Sass 単体利用

node-sass による CLI ビルドを試す。

プロジェクト構成。

プロジェクト構成は以下。

.
├── package.json
└── src
    ├── assets
    │   └── index.html
    └── scss
        ├── App.scss
        ├── Base.scss
        ├── Content.scss
        ├── Footer.scss
        ├── Header.scss
        └── Variables.scss

src/scss へ CSS を格納。これを開発版なら src/assets、リリース版は dist/ に変換して bundle.css というファイル名で出力する。開発版では Source Maps にも対応。必要な npm は node-sass のみ。

$ npm i -D node-sass

npm-scripts

Sass は Stylus と同様に CLI オプションのみで設定を完結可能。package.json における定義は以下。

{
  "scripts": {
    "build": "node-sass ./src/scss/App.scss ./src/assets/bundle.css -r --source-map true --output-style expanded",
    "watch": "node-sass ./src/scss/App.scss ./src/assets/bundle.css -r -w --source-map true --output-style expanded",
    "release": "node-sass ./src/scss/App.scss ./dist/bundle.css -r --output-style compressed",
  }
}

オプションについて現時点の公式ページ資料だとわかりにくいため、使用しているものに絞って補足。

オプション 内容
-r ファイルやフォルダーを再帰的に監視する。フォルダーで階層構造を持たせたくなるかもしれないため、指定。
-w ファイルの変更を監視して自動変換する。
--source-map Source Maps を出力する。Boolean 値を指定する必要あり。
--output-style 出力される CSS のスタイル。expanded はネストを展開してくれる。規定値の nest だとインデントでネストっぽくなるけど余計である。なぜこれを規定値にしたのか。compressed は minify。リリース版で指定する。

変換してみる

npm-scripts に定義されたコマンドを実行して CSS.next なファイル群をひとつの CSS に変換してみる。コマンドとしては

コマンド 役割
build 開発用。Source Maps あり。単体で変換結果をチェックする場合に使用する。
watch 開発用。Source Maps あり。ファイルを監視して変更を検知したら CSS を自動変換する。browser-sync による Web ブラウザー自動表示も設定している。開発時は通常、こちらを使用する。
release リリース用。Source Maps なし、minify あり。

となるので、それぞれ

$ npm run build
$ npm run watch
$ npm run release

とすればよい。出力されたファイルを見れば適切な変換がおこなわれていることを確認できるはず。

webpack (CSS Modules)

webpack を使用して React から Sass を CSS Modules として参照できるようにする。

プロジェクト構成

プロジェクト構成は以下。

.
├── package.json
├── webpack.config.babel.js
└── src
    ├── assets
    │   └── index.html
    └── js
        ├── App.js
        └── components
            ├── App.js
            ├── Content.scss
            ├── Content.js
            ├── Footer.scss
            ├── Footer.js
            ├── Header.scss
            ├── Header.js
            └── Variables.scss

ファイル出力の仕様は node-sass 版と同じ。webpack 設定が追加されて SCSS ファイルが React コンポーネントに併置されているのが特徴的。

npm

必要な npm をインストール。webpack と Sass 関連だけ、他は割愛。

$ npm i -D webpack css-loader node-sass sass-loader extract-text-webpack-plugin

それぞれの役割をまとめる。

npm 役割
webpack Bundler。JavaScript や CSS の参照を解決、指定されたファイル単位にまとめて出力する。
css-loader webpack loader。CSS の参照を解決してくれる。
node-sass Sass 変換ツール Node 版。 Sass に基づく CSS 変換を担当。
sass-loader webpack loader。node-sass を webpack で利用するためのもの。
extract-text-webpack-plugin webpack plugin。css-loader と sass-loader のテキスト出力を受けて CSS ファイル化する。

webpack.config.babel.js

webpack の設定は以下。JavaScript 分も含まれているため長いが、すべて掲載する。

import WebPack from 'webpack'
import MinifyPlugin from 'babel-minify-webpack-plugin'
import ExtractTextPlugin from 'extract-text-webpack-plugin'

export default (env) => {
  const PROD = !!(env && env.prod)

  return {
    entry: './src/js/App.js',
    output: {
      path: PROD ? `${__dirname}/dist` : `${__dirname}/src/assets`,
      filename: 'bundle.js',
      publicPath: '/'
    },
    devtool: PROD ? '' : 'source-map',
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader'
          }
        },
        {
          test: /\.scss$/,
          use: ExtractTextPlugin.extract([
            {
              loader: 'css-loader',
              options: {
                modules: true,
                importLoaders: 1,
                sourceMap: !(PROD),
                minimize: PROD ? { autoprefixer: false } : false
              }
            },
            {
              loader: 'sass-loader',
              options: {
                outputStyle: PROD ? 'compressed' : 'expanded',
                sourceMap: !(PROD)
              }
            }
          ])
        }
      ]
    },
    devServer: {
      contentBase: './src/assets'
    },
    plugins: PROD ? [
      new WebPack.DefinePlugin({
        'process.env.NODE_ENV': JSON.stringify('production')
      }),
      new MinifyPlugin({
        replace: {
          'replacements': [
            {
              'identifierName': 'DEBUG',
              'replacement': {
                'type': 'numericLiteral',
                'value': 0
              }
            }
          ]
        }
      }, {}),
      new ExtractTextPlugin({ filename: 'bundle.css' })
    ] : [
      // development
      new ExtractTextPlugin({ filename: 'bundle.css' })
    ]
  }
}

PostCSS + cssnext のサンプルと見比べてもらえるとわかるけど

{
  loader: 'sass-loader',
  options: {
    outputStyle: PROD ? 'compressed' : 'expanded',
    sourceMap: !(PROD)
  }
}

ぐらいしか変更していない。sass-loaderoptions に指定したものは node-sass へそのまま渡される。よって Source Maps や minify 指定はここでおこなう。

実際に変換してみると、React コンポーネントで参照している className がハッシュ値に変換されて CSS 側もそうなっていることを確認できるはず。

まとめ

AltCSS として既に Stylus に馴染んでおり、SCSS の記法は似通っているので違和感はなかった。CSS Modules も PostCSS とほとんど同じ。要素技術としてうまいこと可換にできた感がある。

最後に私がこれまで試した AltCSS 所感をまとめておく。

  • Stylus
    • Stylus を使ってみる が 2015/1/14 なので 3 年ぐらい利用したことになる
    • 当時は Sass の Ruby 依存を忌避して Stylus を採用
    • Pure Node、単体で高機能、開発活発、記述の柔軟性からとても気に入っていた
    • 開発は停滞したものの、現時点の選択肢としては悪くない
    • 個人的には開発が活発化して復活してほしい
  • PostCSS + cssnext
    • 昨年末に試してみた
    • CSS 記法のプラグイン拡張と cssnext の存在から Babel と babel-preset-env 的なものを期待していた
    • cssnext が csswg にないものを採用しており運用に不安がある
    • CSS の将来仕様で Mix-In やネスト (こちらは csswg にない) といった便利機能に相当するものがサポートされないっぽくて微妙
    • PostCSS としては Stylus/Sass から乗り換えたくなるプラグインは今のところない
    • プラグイン拡張に可能性を感じるならあり
    • 他の AltCSS 相当な機能をプラグインかき集めて実現するのはきついので cssnext のようなプリセットを検討したほうがよさそう
  • Sass
    • 記法は Stylus から CSS 標準記法の省略を抜いた感じ
    • Stylus の省略記法に馴染んでると辛いが、それなら SCSS でなく SASS を採用すればよい
    • 私の必要とする機能においては透過的変数をのぞき Stylus と同等
    • Stylus 代替としてはこれかな

これら 3 種について Node 版を npm trends で比較 した結果は 2018/1/16 時点だと PostCSS が Sass にダブル スコア以上の差をつけてトップだった。なんとなく Sass が最も普及していると予想していたので、これは意外。もしかすると gem 版ユーザーが圧倒的に多いとか?gem (Ruby) 版 Sass のダウンロード数がバージョンではなく期間推移も集計していれば npm trends のものと足して比較できたのに。

とりあえず現時点で選ぶとしたら Sass が無難だろう。乗り捨てを考慮しても SCSS 記法にしておけばロック イン度合いも少なくて Stylus/PostCSS への移行しやすいはず。


COMMENTS

  • yassh
    2018年1月22日 11:13 AM 返信

    ベンダー接頭辞の問題にはどう対処していますか?

    私の場合、SCSSを選択した場合でも、Autoprefixerを使うために最後にはPostCSSに通しています。
    Autoprefixerがあるために、完全にPostCSSを捨て去ることはまだできていません。

    • 2018年1月22日 8:15 PM 返信

      私はベンダー接頭辞はなるべく使用しない主義なので残念ながら「対処しない」となります。
      ちなみに使用しない理由は

      * 実験的な機能を試すことが目的なのでプロダクトには適さない
      * ベンダー接頭辞が廃止 (代替としてランタイム フラグ) されてゆく方向にある

      です。iOS Safari など特別な環境でやむを得ず指定することもありますが、そのようにしたことを
      強く明示するため Autoprefixer のように暗黙的な挿入は避けます。

      これはあくまで私の理由ですので Autoprefixer を採用すること自体は否定しません。

  • yassh
    2018年1月23日 5:06 PM 返信

    なるほど、ありがとうございます。

REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です