webpack3+Sassのビルド 〜PostCSSとExtractTextPluginを添えて〜

初投稿です。優しくしてください。

モジュールバンドラと聞いて、webpack、gulp、Parcel、Grunt・・・
とまあぱっと思いつくのはだいたいその辺ですが、できることってぶっちゃけどれもほぼ一緒だよね感（一長一短ありますが）。
フロントエンドツール多すぎ問題。

ってことで入門的にとりあえず流行ってるし使いやすそうなので、webpack3を触ってみました。

# そもそもwebpackってなに？
> CSSやJavaScript、画像などのWebコンテンツを構成するあらゆるファイルを「モジュール」という単位で取り扱い、「バンドル」という１つのファイルに最適な形で変換するためのツールです。

そのままですが、要は複数のファイルを一つのファイルにまとめるツールです。
それによりファイルのリクエスト数を減らし、ページ読み込み速度の改善が見込めます。

JavaScriptのビルドに使用されることがほとんどかと思いますが、今回はSassのビルドについて触れていこうと思います。

Sassだって仲間に入れてほしい。

# やりたいこと
- SassからCSSへのコンパイル
- SorceMapも出力したい
- 特定コメント以外をminify
- ベンダープレフィックス自動付与（Compass廃止したい）
- Media Queryの最適化（Sassの場合ファイル肥大化の要因となる）
- コーディング規約に遵守しているかチェックしてほしい（記述統一、レビュー負担軽減）


# 事前準備
node.jsが入っているか確認し、入っていないようであればインストールしてパスを通してください。

```
$ node -v
v8.9.4
```

# 必要な設定ファイル
設定ファイルはまとめて記述することも可能ですが、個人的な好みで分けました。

- package.json
- webpack.config.js
- postcss.config.js
- stylelint.config.js


# ディレクトリ階層
```text

┃
┣━ sass
┃   ┣━ style.scss
┃   ┣━ _xxx.scss
┃   ┣━ _xxx.scss
┃   ┃
┃   ...
┃
┣━ style.css
┣━ style.css.map
┃
┣━ package.json
┣━ webpack.config.js
┣━ postcss.config.js
┗━ stylelint.config.js

```


# 各設定ファイルの内容と説明
随時更新予定です（たぶん）。

## package.json
パッケージを管理するための設定ファイルです。
`"dependencies"` に必要なプラグインを記述しておくと、 `npm install` するだけで記述したものをすべてインストールしてくれます。

各プラグインのバージョンや依存関係を解決してくれるため環境構築が楽。
mavenでいう `pom.xml` みたいなもの。

```json:package.json
{
  "name": "hello-webpack",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^3.10.0",
    "extract-text-webpack-plugin": "^3.0.2",
    "css-loader": "^0.28.9",
    "sass-loader": "^6.0.6",
    "node-sass": "^4.7.2",
    "postcss-loader": "^2.1.0",
    "autoprefixer": "^7.2.5",
    "cssnano": "^3.10.0",
    "css-mqpacker": "^6.0.2",
    "stylelint-webpack-plugin": "^0.10.1",
    "stylelint": "^8.4.0"
  }
}
```


## webpack.config.js
webpackの設定ファイルです。
あまり大したことは書いていないですが、ここに辿り着くまでに地味にハマったことは下記2点。

- SourceMapは各loaderに記述しないと生成されない。
- loaderの順番をかえるとしぬ。


```js:webpack.config.js
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');

module.exports = {
  entry: {
    style: path.join(__dirname, 'sass', 'style.scss')
  },
  output: {
    path: __dirname,
    filename: '[name].css'
  },
  module: {
    rules: [
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract({
          use: [
            {
              loader: 'css-loader',
              options: { sourceMap: true }
            },
            {
              loader: 'postcss-loader',
              options: { sourceMap: true }
            },
            {
              loader: 'sass-loader',
              options: { sourceMap: true }
            },
          ]
        })
      },
    ],
  },
  devtool: 'source-map',
  plugins: [
    new ExtractTextPlugin('[name].css'),
    new StyleLintPlugin('./stylelint.config.js'),
  ]
};
```


## postcss.config.js
PostCSSの設定ファイルです。
autoprefixer、cssnano、css-mqpackerを読み込んでいます。

autoprefixerではベンダープレフィックスを付与する対象ブラウザやバージョンの指定もできます。

```js:postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')({
      browsers: [ 'last 2 versions' ]
    }),
    require('cssnano'),
    require('css-mqpacker'),
  ],
};
```


## stylelint.config.js
リンターの設定ファイルです。
自分好みのルールに近かったので、[こちら](https://gist.github.com/buchiya4th/f4ca1be2ab98ee5a8098fa68a93e752c)をベースに追加したり削除したり。
リンタールールについては[公式サイト](https://stylelint.io/user-guide/rules/)をご参照ください。

:sparkles: stylelintの記事書きました。
[npmのみでSassのstylelint 〜webpackとPostCSS不要編〜](https://qiita.com/pprhr/items/b61f84944e1e869161cc)

<details><summary>長いため折りたたみ</summary><div>

```js:stylelint.config.js
module.exports = {
  ignoreFiles: [
    'sass/style.scss',
  ],
  rules: {
    'at-rule-name-case': 'lower', // @ルールは小文字指定（大文字を禁止）
    'at-rule-name-space-after': 'always-single-line', // @ルールで単一行の時は@ルール名の後に空白を必須
    'at-rule-semicolon-newline-after': 'always', // @ルールでセミコロンの後に改行を必須
    'block-closing-brace-empty-line-before': 'never', // 閉じ括弧前の空行を禁止
    'block-closing-brace-newline-before': 'always-multi-line', // 複数行の時に閉じ括弧前に改行を必須
    'block-opening-brace-space-after': 'always-single-line', // 単一行の時に閉じ括弧後に空白を必須
    'block-opening-brace-space-before': 'always', // 単一行で閉じ括弧前に空白を必須
    'color-hex-case': 'lower', // hex値は小文字指定（大文字を禁止）
    'color-hex-length': 'short', // hex値は短い表記（冗長な表記は禁止）
    'color-named': 'never', // 名前付きカラー指定を禁止
    'color-no-invalid-hex': true, // 無効な16進数の色指定を禁止
    'declaration-bang-space-after': 'never', // 宣言後の空白を禁止
    'declaration-bang-space-before': 'always', // 宣言前の空白を必須
    'declaration-block-no-duplicate-properties': [ // 宣言ブロック内で重複するプロパティを禁止
      true,
      {
        'ignore': [
          'consecutive-duplicates' // 連続する場合は許可（レガシーブラウザ対応用）
        ]
      }
    ],
    'declaration-block-no-shorthand-property-overrides': true, // ショートハンドで値の上書き禁止
    'declaration-block-semicolon-newline-after': 'always-multi-line', // セミコロン後に改行が必須（セレクタ含め一行の場合は容認）
    'declaration-block-semicolon-newline-before': 'never-multi-line', // セミコロン前の空白・改行を禁止
    'declaration-block-semicolon-space-after': 'always-single-line', // 単一行の時のセミコロン後に空白が必須
    'declaration-block-semicolon-space-before': 'never', // セミコロン前の空白を禁止
    'declaration-block-trailing-semicolon': 'always', // 末尾のセミコロンを必須
    'declaration-colon-newline-after': 'always-multi-line', // 複数行の時のコロン後には改行が必須
    'declaration-colon-space-after': 'always-single-line', // 単一行の時のコロン後に空白が必須
    'declaration-colon-space-before': 'never', // コロン前の空白を禁止
    'function-comma-space-after': 'always-single-line', // 単一行のカンマ後には空白が必要
    'function-comma-space-before': 'never', // カンマ前の空白を禁止
    'function-parentheses-space-inside': 'always', // 関数の括弧内側に空白を1つ
    'indentation': 2, //インデントを制御
    'length-zero-no-unit': true, // 0の時の右記単位指定を禁止（em, ex, ch, vw, vh, cm, mm, in, pt, pc, px, rem, vmin, vmax）
    'max-empty-lines': 2, // 隣接する空行を制限
    'media-feature-colon-space-after': 'always', // メディアクエリ内のコロンの後の空白を指定
    'media-feature-colon-space-before': 'never', // メディアクエリ内のコロンの前の空白を禁止
    'media-feature-name-case': 'lower', // メディアクエリの小文字指定（大文字を禁止）
    'media-feature-parentheses-space-inside': 'never', // メディアクエリの括弧内側の空白を禁止
    'media-feature-range-operator-space-after': 'always', // メディアクエリ内の範囲演算子の後の空白を指定
    'media-feature-range-operator-space-before': 'always', // メディアクエリ内の範囲演算子の前の空白を指定
    'media-query-list-comma-newline-after': 'always-multi-line', // メディアクエリの複数行の時はカンマの後に改行を必須
    'media-query-list-comma-newline-before': 'never-multi-line', // メディアクエリの複数行の時はカンマの前に空白を必須
    'media-query-list-comma-space-after': 'always-single-line', // メディアクエリのカンマの後に改行を必須
    'media-query-list-comma-space-before': 'never', // メディアクエリのカンマの前の空白を禁止
    'no-extra-semicolons': true, // 余分なセミコロンを禁止
    'no-missing-end-of-source-newline': true, // 行末の改行を必須
    'number-leading-zero': 'never', // 「0.*」の0は省略し許可しない（.*のようにする）
    'number-no-trailing-zeros': true, // 小数点以下の末尾0を禁止
    'property-case': 'lower', // プロパティは小文字指定（大文字を禁止）
    'selector-attribute-brackets-space-inside': 'never', // 属性セレクタ内の括弧の内側の空白を禁止
    'selector-attribute-operator-space-after': 'never', // 属性セレクタ内の演算子の後の空白を禁止
    'selector-attribute-operator-space-before': 'never', // 属性セレクタ内の演算子の前の空白を禁止
    'selector-attribute-quotes': 'always', // 属性値の引用符を必須
    'selector-combinator-space-after': 'always', // コンビネータ後に空白を必須
    'selector-combinator-space-before': 'always', // コンビネータ前に空白を必須
    'selector-descendant-combinator-no-non-space': true, // 下位コンビネータの空白以外を禁止
    'selector-list-comma-newline-before': 'never-multi-line', // セレクタリストのカンマの前の空白を禁止
    'selector-list-comma-space-before': 'never', // セレクタリストのカンマの前の空白を禁止
    'selector-max-empty-lines': 1, // セレクタ内の隣接する空行の数を制限
    'selector-max-specificity': '0,4,0', // 'id,class,type'でセレクタの特異性を制限
    'selector-pseudo-class-case': 'lower', // 擬似セレクタは小文字指定（大文字は禁止）
    'selector-pseudo-class-parentheses-space-inside': 'never', // 擬似セレクタ内の括弧の内側の空白を禁止
    'selector-type-case': 'lower', // セレクタは小文字（大文字は禁止）
    'shorthand-property-no-redundant-values': true, // 省略形のプロパティで重複する値を許可しない
    'string-no-newline': true, // 文字列の改行を禁止
    'string-quotes': 'double', // '指定（''を禁止）
    'unit-case': 'lower', // 単位は小文字指定（大文字を禁止）
    'unit-no-unknown': true, // 不明な単位を禁止
    'value-list-comma-newline-after': 'always-multi-line', // 値リストのカンマ後に改行は許可し改行後のカンマは禁止
    'value-list-comma-space-after': 'always-single-line', // 値リストの単一行のカンマ後には空白が必要
    'value-list-comma-space-before': 'never', // 値リストのカンマ前の空白を禁止
    'value-no-vendor-prefix': true, // ベンダープレフィックスを禁止（autoprefixerの使用前提）
  },
}
```
</div></details>


# プラグインについて
**:star: webpack**
今回の主役です。

**:star: extract-text-webpack-plugin**
`style-loader` の代替として使用します。
`style-loader` は指定したCSSファイルを `<style>` タグに入れ込むようなJavaScriptを生成してくれますが、今回はCSSファイルそのものを出力したいためloaderではなくこちらのプラグインを使用。
「loaderが実行した結果」をテキストとして吐き出してくれるため、出力対象のファイルはCSSに限りません。

**:star: css-loader**
urlや `@import` など、CSSファイルの依存関係の解決をしてくれます。

**:star: sass-loader**
**:star: node-sass**
Sassのコンパイルに使用。
他のCSSメタ言語、例えばlessであれば `less-loader` を使用します。

**:star: postcss-loader**
必須ではないですが、CSSの事後処理として使用します。
PostCSSを使用するとCompassのおよそ43倍パフォーマンスが向上するとかなんとか。
参考：[PostCSS とは何か](https://speakerdeck.com/jmblog/postcss-tohahe-ka)

下記プラグインはPostCSSをパーサーとして利用しています。
**・** autoprefixer（ベンダープレフィックス自動付与）
**・** cssnano（CSSのminify）
**・** css-mqpacker（Media Queryの最適化）

**:star: stylelint-webpack-plugin**
**:star: stylelint**
設定した内容に沿ってSassの記述をチェックしてくれます。


# Sassの変更を監視
```shell
$ webpack --watch
```

ちなみに、 `package.json` の `scripts` にコマンドのエイリアスを設定できます。

```json:package.json
"scripts": {
  "watch": "webpack --watch"
}
```

```shell
$ npm run watch
```


# 感想
- npmだけで完結しているため導入もシンプルで分かりやすい。
- これくらいであればビルドの時間も気になりませんでした。
- リンターはCSScombと組み合わせるとさらに捗りそう。チーム内での記述が統一できるのでレビュー負担も軽減されそうです。
- SourceMapは各loaderそれぞれに記述しないと生成してくれないってのがちょっとモサい。
- PostCSSがやりたいことをほとんど実現してくれました。