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のファイル名にハッシュを付けるかどうかだけです。
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
に以下を追加します。
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
を作成します。
import './main.scss';
内容はこれだけですね。
既にcssディレクトリにjsファイルがあるという危険な状況ですが、そのまま進みます。
(このファイルは src/js/style.js
など、なんでも構いません。個人的にはどっちもどっちだなぁとw)
次にwebpackの設定です。設定は簡単でentryのファイルに先ほど作成した src/css/index.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となるので、説明は割愛して全体をのせておきます。
本番用
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 }),
],
};
開発用
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を作成します。
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です。
<!DOCTYPE html>
<html>
<head>
...
<%= css_load_tag 'style' %>
</head>
...
</html>
これで、開発環境はSassのビルドでもHMRを有効にしつつ、本番環境ではキャッシュ対策のハッシュをつけたCSSファイルを読み込むということが出来ました。
終わりに
ということでSassのビルドもHMRするために色々頑張ってみました。
Loaderを変えれば、StylusやPostCSSのビルドも同様にできると思います。
まだまだ開発し始めたところですが、画面保持したまま自動的にリロードされるのはストレスなくていいですね。
今回は開発・本番で分けるところまでやりましたが、CSS in JSも一般的になっていますし、気持ち悪さがないのであれば方針2のJSとして出力しちゃうでも充分かなと思います。
とはいえ、冒頭の注意でも書きましたが、そもそもこの構成・・という話もあるので、そこから考えていく必要がありますね。
今回ちょっと頑張って環境つくりましたが、3ヶ月後はどうなっていることやら・・・しっかり追っていきたいところです。