SassのビルドもWebpackでHot Module Replacementしたい


[Rails + モダンJS環境で新規アプリ作成](http://qiita.com/UFO/items/3c4e6a323e86c0c64e04)の続きです。

上の記事ではRailsのSprocketsを切って、JSの環境をnpmによせました。となってくると、
Sassのビルドもnpmによせたい！ただ、gulpなどは使いたくない！
ということで、WebpackにSassのビルドも任せて、CSSでもHot Module Replacementをしようとやってみました。以下、CSSのソースは `src/css/` に置くこととしています。

前回と、今回で作成したサンプルはこちらです。
https://github.com/ufotsuboi/rails-webpack-sample


# 注意

今回はCSSは全体に読み込む、JSは一部のみといった、JSとCSSを完全にわけて扱いたいような状況を想定しています。
SPAのようなJSもCSSも全部のページで読み込むといったような状況であれば、webpackのloaderを使い、いわゆるCSS in JSしてしまえばHMRが有効になりますのでそちらをおすすめします。

# 事前準備

どの方法でもloaderが必要になるので、予め必要なライブラリをインストールしておきます。

```bash
# 方針2ではextract-text-webpack-pluginは必要ありません。
$ npm install --save-dev style-loader css-loader sass-loader node-sass extract-text-webpack-plugin
```

# 方針1: sass-loaderとextract-text-webpack-pluginでcssファイルとして出力

結論からいうと、これではHMRは効きません。（これでできると楽だったのですが）

内容としては参考にさせていただいた以下の記事とほぼ同じです。
[webpackを使ってsassをコンパイルできるようにしよう！](http://qiita.com/nicchi__1985/items/e30e73de6d8443909537)


Webpackの設定を追加していきます。本番環境と開発環境の違いは `extract-text-webpack-plugin` で出力するcssのファイル名にハッシュを付けるかどうかだけです。

```webpack.config.js
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 追加

module.exports = {
  
  ...
  
  module: {
    loaders: [
	  
	  ...,
	  
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader'),
      },
    ],
  },
  plugins: [
	...,
    new ExtractTextPlugin('[name].css', { allChunks: true }),  // 本番は '[name]-[hash].css'
  ],
  
  ...
  
};
```

こうすることによって, JSでCSSファイルが読み込めるようになり、かつその部分だけを抜き出した `bundle.css` が出力されます。ですので、エントリーポイントとなる `main.js` に以下を追加します。

```javascript:src/js/main.js
import '../css/main.scss';

...


```

が、冒頭に述べたように、この方法ではCSSを変更すると再ビルドはされるのですが、JS部分の更新しか行われないため変更されません。HMRではなく、ブラウザをリロードしたいだけならこれでも問題なくリロードされます。ただ、今回やりたいのはあくまでHMRなので別の方法を考えてみました。

（[この方針で実装したブランチ](https://github.com/ufotsuboi/rails-webpack-sample/tree/extract-plugin)）

# 方針2: CSSもJSとして出力しよう。

...はい。なんか言い出しましたね。

ようはJSとして出力すればHMRが効きます。なので `extract-text-webpack-plugin` で別に出力するのではなく、異なるエントリーポイントを用意して、CSSだけ含まれたJSファイルを出力してCSSとしてではなくJSとしてheadで読み込みましょう。

まぁ、とりあえずやってみましょう。まずは、新たにエントリーポイントである `src/css/index.js` を作成します。

```javascript:src/css/index.js
import './main.scss';
```

内容はこれだけですね。
既にcssディレクトリにjsファイルがあるという危険な状況ですが、そのまま進みます。
（このファイルは `src/js/style.js` など、なんでも構いません。個人的にはどっちもどっちだなぁとｗ）

次にwebpackの設定です。設定は簡単でentryのファイルに先ほど作成した `src/css/index.js` を追加するだけです。

```javascript:webpack.config.js

...


module.exports = {
  entry: {
    bundle: './src/js/main.js',
    style: './src/css/index.js', // 追加
  },
  
  ...
  
};
```

これで、前回のように設定していれば、開発環境はwebpack-dev-serverが配信、本番環境ではハッシュ付きの `style.js` が出力されるので、これを読み込んでやればOKです。そうすると `style.js` が、head内に `text/css` としてビルドしたCSSを挿入してくれます。

もちろん、JSとして読み込んでいるので開発環境でCSSを変更を変更すると、HMRによって画面リロードすることなく変更が適応されます。

（[この方針で実装したブランチ](https://github.com/ufotsuboi/rails-webpack-sample/tree/output_as_js)）

便利！！

...なのですが、さすがに本番環境まで `style.js` は気持ち悪い・・・

# 方針3: 開発環境はJSとして、本番環境はCSSとして。

ということで、環境によって切り分けちゃいましょう！読み込むファイルが変わるのはRails Helperでゴリッと解消します。

Webpackの設定は本番環境が方針1、開発環境が方針2となるので、説明は割愛して全体をのせておきます。

### 本番用

```javascript:webpack.config.prod.js
const path = require('path');
const webpack = require('webpack');
const ManifestPlugin = require('webpack-manifest-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
    style: './src/css/index.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name]-[hash].js',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader'),
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
    new webpack.optimize.UglifyJsPlugin({
      minimize: true,
      compress: {
        warnings: false,
      },
    }),
    new ManifestPlugin(),
    new ExtractTextPlugin('[name]-[hash].css', { allChunks: true }),
  ],
};

```

### 開発用

```javascript:webpack.config.dev.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    bundle: './src/js/main.js',
    style: './src/css/index.js',
  },
  output: {
    path: path.join(__dirname, 'public/dist'),
    filename: '[name].js',
    publicPath: 'http://localhost:8080/',
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.scss$/,
        loader: 'style-loader!css-loader!sass-loader',
      },
    ],
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
    }),
  ],
  devServer: {
    contentBase: 'public/dist',
  },
};
```

この場合、 `npm run build` すると不必要な `style-[hash].js` も出力されてしまいますが、中身は空ですし別にいいかなとｗ

### 環境によって読み込みタグを切り替えるHelper作成

次にCSSを読み込む、Rails Helperを作成します。

```app/helpers/application_helper.rb
module ApplicationHelper
  def css_load_tag(file_name)
    if Rails.env.development?
      javascript_include_tag webpack_dev_server_path("#{file_name}.js")
    else
      stylesheet_link_tag assets_path("#{file_name}.css")
    end
  end

  def assets_path(path)
    return webpack_dev_server_path(path) if Rails.env.development?
    manifest = Rails.application.config.assets_manifest
    path = manifest[path] if manifest && manifest[path].present?
    "/dist/#{path}"
  end

  private
  def webpack_dev_server_path(path)
    "http://localhost:8080/#{path}"
  end
end
```

簡単に説明すると、開発環境では、webpack-dev-serverのURLでjsファイルを読み込む。本番環境ではハッシュ付きのCSSファイルを読み込むヘルパーです。これをheadで読み込んでやればOKです。

```app/view/layout/application.html.erb
<!DOCTYPE html>
<html>
  <head>
	...
    <%= css_load_tag 'style' %>
  </head>
  
  ...
  
</html>
```

これで、開発環境はSassのビルドでもHMRを有効にしつつ、本番環境ではキャッシュ対策のハッシュをつけたCSSファイルを読み込むということが出来ました。

# 終わりに

ということでSassのビルドもHMRするために色々頑張ってみました。
Loaderを変えれば、StylusやPostCSSのビルドも同様にできると思います。

まだまだ開発し始めたところですが、画面保持したまま自動的にリロードされるのはストレスなくていいですね。
今回は開発・本番で分けるところまでやりましたが、CSS in JSも一般的になっていますし、気持ち悪さがないのであれば方針2のJSとして出力しちゃうでも充分かなと思います。
とはいえ、冒頭の注意でも書きましたが、そもそもこの構成・・という話もあるので、そこから考えていく必要がありますね。

今回ちょっと頑張って環境つくりましたが、3ヶ月後はどうなっていることやら・・・しっかり追っていきたいところです。