52
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

Rails + モダンJS環境で新規アプリ作成の続きです。

上の記事では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が必要になるので、予め必要なライブラリをインストールしておきます。

# 方針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をコンパイルできるようにしよう!

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 に以下を追加します。

src/js/main.js
import '../css/main.scss';

...


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

この方針で実装したブランチ

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

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

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

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

src/css/index.js
import './main.scss';

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

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

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によって画面リロードすることなく変更が適応されます。

この方針で実装したブランチ

便利!!

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

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

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

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

本番用

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 }),
  ],
};

開発用

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 も出力されてしまいますが、中身は空ですし別にいいかなとw

環境によって読み込みタグを切り替える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ヶ月後はどうなっていることやら・・・しっかり追っていきたいところです。

52
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
52
46

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?