webpackとBabelの基本を理解する(5) ―Sass編―

独学の内容をまとめたものです。誤りがございましたら、ご連絡いただけると幸いです。

**リンク**

1. [webpackとBabelの基本を理解する(1) ―webpack編―](https://qiita.com/koedamon/items/3e64612d22f3473f36a4)
2. [webpackとBabelの基本を理解する(2) ―Babel編―](https://qiita.com/koedamon/items/92c986456e4b9e845acd)
3. [webpackとBabelの基本を理解する(3) ―webpackとBabel編―](https://qiita.com/koedamon/items/6cf2201be78c3d79516d)
4. [webpackとBabelの基本を理解する(4) ―React編―](https://qiita.com/koedamon/items/8fb75a7c001da40c7bea)
5. webpackとBabelの基本を理解する(5) ―Sass編―（本記事）

# 概要
## この記事の概要

- 目的
  - フロントエンドの環境構築に利用されるツールへの理解を深める
- 本記事のゴール
  - SassのコードをコンパイルしてHTMLに埋め込む方法を知る
  - バンドルされたCSSファイルを別ファイルに切り出す方法を知る
- 対象者
  - WEBフロント担当者
  - HTML,CSS,JavaScript(es2015含む)の基本的な構文を理解している人
  - npmの利用方法を理解している人
- 環境・バージョン
  - Windows10
  - Node.js(推奨版) 10.15.01
  - npm 6.4.1
  - webpack 4.29.6
  - webpack-cli 3.2.3
  - css-loader 2.1.0
  - node-sass 4.11.0
  - sass-loader 7.1.0
  - style-loader 0.23.1
  - mini-css-extract-plugin 0.4.5

## Sass
Sassそのものの説明は割愛させていただきます。。。

[Sass](https://sass-lang.com/)
[Sass | Wikipedia](https://ja.wikipedia.org/wiki/Sass)

Dreamweaverでは標準でSassコードのコンパイル機能があるそうですね。VSCodeも便利なアドオンがあるようで。

# インストール
webpackでJavaScriptとJSON以外のファイルを扱いたい場合は、対応するloaderが必要でしたね。以下4つのパッケージをインストールします。

- style-loader: CSSを&lt;style&gt;タグでHTMLに埋め込む
- css-loader: CSSファイルをJS用モジュールに変換する
- sass-loader: SassファイルをCSSに変換するツールを呼び出す
- node-sass: Sassをコンパイルするためのツール

```
$ npm install style-loader css-loader sass-loader node-sass --save-dev
```

# CSS・Sassをバンドルする
## webpack.config.jsの記述
webpack.config.jsに、CSS・Sassファイルをバンドルする際のルールを追記します。

```js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: { url: false }
          },
          'sass-loader'
        ]
      }
    ]
  }
};
```

対象ファイルは、`.css, .sass, .scss`です。sass-loader → css-loader → style-loader の順に実行されます。

**options.url**
CSS内に画像への参照`url(image.png)`が含まれる場合、それもバンドルの対象となります。画像ファイルをbase64でエンコードするとかでは無い限り、必要な無いので、`options.url`に`false`を指定して対象外とします。（初期値はtrue）

## JSファイルとの接続
webpackは、起点となるJSファイルからimport文を頼りに芋づる式にファイルをつなげていくツール、でしたね。ということは、起点ファイルとCSSファイルの接点を作る必要があります。entryに指定しているJSファイルに、CSSファイルのインポート文を記述します。

```js
import 'file.scss';
```

**CSS In JavaScript**
sass-loaderでCSSに変換され、css-loaderでJSモジュールに変換されたデータは、style-loaderによって、`<style>`タグでHTMLのhead内に埋め込まれます。
正確には、バンドル後のJSファイルにstyle情報とstyleタグを埋め込む処理が追加されます。その為、JSファイルを読み込んだだけでCSSファイルの情報も反映されるようになります。

[css-loader | webpack](https://webpack.js.org/loaders/css-loader/)
[style-loader | webpack](https://webpack.js.org/loaders/style-loader/)
[sass-loader | webpack](https://webpack.js.org/loaders/sass-loader/)

# CSSファイルを分離する
## Plugins
開発中はstyleタグで埋め込みでも良いが、最終的にはCSSファイルは別にしたいという場合も多いと思います。その場合は専用のプラグインを利用します。
これまでに登場したLoaderは、特定のタイプのデータをJSのモジュールに変換する役目を果たします。それ以外のことをやりたい場合は、プラグイン（Plugins）を使って実現します。

[Plugins | webpack](https://webpack.js.org/concepts/plugins/)
## MiniCssExtractPluginのインストール
「MiniCssExtractPlugin」を使って分離手術を実施したいと思います。

```console
$ npm install mini-css-extract-plugin --save-dev
```

[MiniCssExtractPlugin | webpack](https://webpack.js.org/plugins/mini-css-extract-plugin/)

## webpack.config.jsの設定
webpack.config.jsの記述例はこちら。

```
# ディレクトリ構成
sample/
  ├ src/
  │   ├ scss/
  │   │   ├ base.scss
  │   │   └ index.scss
  │   └ index.js 
  ├ dist/
  └ webpack.config.js
```

```js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'js/sample.js'  // /dist/js/sample.jsに出力
  },
  module: {
    rules: [
      {
        test: /\.(sa|sc|c)ss$/,
        exclude: /node_modules/,
        use: [
          MiniCssExtractPlugin.loader, // style-loaderの代わり
          {
            loader: 'css-loader',
            options: { url: false }
          },
          'sass-loader'
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: 'css/sample.css',  // /dist/css/sample.cssに出力
    })
  ]
};
```

冒頭で`mini-css-extract-plugin`をインポートしています。webpack.config.jsはNode.jsのファイルなので`require`文です。
`style-loader`の代わりに`MiniCssExtractPlugin.loader`を利用するようにしています。

**plugins**
loaderで捕捉したCSSモジュールを別ファイルに切り出すため、インポートしたプラグインのインスタンスを`plugins`に設定します。
コンストラクタに出力したいファイル名を`filename`で渡します。出力先フォルダは`output.path`で指定しているフォルダです。ここでは、`/dist`の配下に`/js`と`/css`でそれぞれフォルダが分かれて出力されるようにしています。

# SCSSファイルをコンパイルして出力する
## フォルダとファイルの準備
上記のディレクトリ構成で、JSファイルとSCSSファイルを準備します。

JSの起点ファイルには、Sass側の起点ファイルとの接点を設けます。

```js
/**
 * /src/index.js
 */
import './scss/index.scss';
```

適当にサスサスしたコードを用意します。CSSファイルで納品なんて場合は、出力後の見え方も意識しておいた方が良いかもしれませんね。Sass側のimportは、webpackではなくnode-sassが対応してくれます。

```scss
/**
 * /scss/base.scss
 */
html {
  font-size: 62.5%;
}
body {
  font-family: Roboto, 'メイリオ', Arial, sans-serif;
  font-size: 1.4rem;
  line-height: 1.3;
  color: #333;
  background-color: #fdf7eb;
}
a{
  -webkit-tap-highlight-color: rgba(0,0,0,0);
  &:link, &:hover, &:visited, &:active {
    text-decoration: none;
    color: inherit;
  }
}

@mixin mediaQuery {
  @media screen and(max-width: 359px){
    @content;
  }
}

/**
 * /scss/index.scss
 */
@import 'base';

%spreadsheet {
  background: url(/img/sprite.png) no-repeat left top;
  background-size: 200px auto;
  overflow: hidden;
  text-indent: -9999px;
}

%BlockContainer {
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

%BlockSize {
  width: 50%;
  height: 60%;
  @include mediaQuery {
    width: 70%;
    height: 80%;
  }
}

.areaA {
  @extend %BlockContainer;
  background-color: #b8d684;
  &__card {
    @extend %BlockSize;
    background-color: #527516;
  }
}

.areaB {
  @extend %BlockContainer;
  background-color: #c8b7cc;
  &__card {
    @extend %BlockSize;
    background-color: #661675;
  }
}

.icon {
  @extend %spreadsheet;
  width: 2rem;
  height: 2rem;

  $grid:
    '0 0',
    '0 -60px',
    '0 -120px',
    '-60px 0',
    '-60px -60px',
    '-60px -120px'
  ;
  @each $value in $grid {
    $i: index($grid, $value);
    &--#{$i} {
      background-position: #{$value};
    }
  }
}

.iconB {
  @extend %spreadsheet;
  width: 6rem;
  height: 4rem;
  background-position: 0 -80px ;
}
```

## 実行結果
webpackを実行した結果はこちら。二つのファイルが結合され、CSSコードに変換されていますね。webpackをwatchモードにした場合、Sassファイルも監視対象に含まれます。

```css
@charset "UTF-8";
html {
  font-size: 62.5%; }

body {
  font-family: Roboto, 'メイリオ', Arial, sans-serif;
  font-size: 1.4rem;
  line-height: 1.3;
  color: #333;
  background-color: #fdf7eb; }

a {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
  a:link, a:hover, a:visited, a:active {
    text-decoration: none;
    color: inherit; }

.iconA, .iconB {
  background: url(/img/sprite.png) no-repeat left top;
  background-size: 200px auto;
  overflow: hidden;
  text-indent: -9999px; }

.areaA, .areaB {
  width: 100%;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center; }

.areaA__card, .areaB__card {
  width: 50%;
  height: 60%; }
  @media screen and (max-width: 359px) {
    .areaA__card, .areaB__card {
      width: 70%;
      height: 80%; } }

.areaA {
  background-color: #b8d684; }
  .areaA__card {
    background-color: #527516; }

.areaB {
  background-color: #c8b7cc; }
  .areaB__card {
    background-color: #661675; }

.iconA {
  width: 2rem;
  height: 2rem; }
  .iconA--1 {
    background-position: 0 0; }
  .iconA--2 {
    background-position: 0 -60px; }
  .iconA--3 {
    background-position: 0 -120px; }
  .iconA--4 {
    background-position: -60px 0; }
  .iconA--5 {
    background-position: -60px -60px; }
  .iconA--6 {
    background-position: -60px -120px; }

.iconB {
  width: 6rem;
  height: 4rem;
  background-position: 0 -80px; }
```



**日本語**
以前に用意した下記構成では、日本語はエンコードされていました。引用符でくくられた文字列はエスケープする仕様でした。エスケープは、css-loaderが担っているようです。

```
// 各パッケージのバージョン
{
  "devDependencies": {
    "css-loader": "^1.0.1",
    "mini-css-extract-plugin": "^0.4.5",
    "node-sass": "^4.10.0",
    "sass-loader": "^7.1.0",
    "webpack": "^4.16.3"
  }
}

// コンパイル前
font-family: Roboto, 'メイリオ', Arial, sans-serif;

// コンパイル後
font-family: Roboto, '\u30e1\u30a4\u30ea\u30aa', Arial, sans-serif;
```

この構成では日本語がそのまま出力されていました。css-loaderがメジャーアップデートしています。仕様が変わったみたいですね。

```js
// 各パッケージのバージョン
{
  "devDependencies": {
    "css-loader": "^2.1.0",
    "mini-css-extract-plugin": "^0.5.0",
    "node-sass": "^4.11.0",
    "sass-loader": "^7.1.0",
    "webpack": "^4.29.1"
  }
}
```

[CSS Loader の文字列エスケープがなんかちょっとおかしい件](https://jeffreyfrancesco.org/weblog/2017041401/)

# ファイルの圧縮
webpackのモードをproductionにしていても、切り出されたCSSファイルは圧縮されません。
以前は、css-loaderのオプションに`minimize`というのがありましたが、最新版では無くなりました。webpack5では、CSSの圧縮が標準装備されるらしいです。
webpack4でCSSの圧縮を行うには、プラグインを利用します。これがまた結構ややこしそうなので、また別の機会に…。下記のサイトが詳しいです…。

[webpack@4 でCSSを抽出する際は mini-css-extract-plugin を使う](https://numb86-tech.hatenablog.com/entry/2018/10/24/221130)
[Minimizing For Production](https://webpack.js.org/plugins/mini-css-extract-plugin/#minimizing-for-production)

近いうちにちゃんと噛み砕いて自分の言葉でまとめたいです。でもその前にwebpack5の安定版が出るかな。どうでしょう。

## 2019年10月22日追記
[sass-loaderの8月の更新(v7.3.0)](https://github.com/webpack-contrib/sass-loader/releases/tag/v7.3.0)にて、productionモードの際に圧縮されるようになりました！
あくまでsass-loaderの方なので、css-loaderだけ利用する場合は引き続きプラグインが必要そうです。


# 参考情報
[Sass（SCSS）でCSSコーディングを効率化・メリットと使い方を知る](https://tech.qookie.jp/posts/info-sass-scss-feature/)
[webpackでCSSやSASSを使う](https://numb86-tech.hatenablog.com/entry/2018/03/03/131414)
[Adobe Dreamweaver CCを使って爆速でSASS（SCSS）環境を手に入れる](https://www.okushin.co.jp/kodanuki_note/2017/03/)
[VSCode(Visual Studio Code)で簡単にSASS/SCSSファイルのコンパイルができる拡張機能「Easy Sass」がお勧め](https://www.karelie.net/vscode-easy-sass/)
[node-sassでSassファイルをコンパイルする](https://qiita.com/setouchi/items/2f7ae68764abe74934fb)