Webpacker を使わずに webpack で頑張る

  • 29
    いいね
  • 0
    コメント

はじめに

Rails で Sprockets を使ったフロントエンドの開発環境を webpack に徐々に移行していく話です。

Rails の開発環境と JavaScript 周りのエコシステムの話は度々話題に上がります。
Rails 5.1 から Webpacker が導入されるようですが、個人的には次のような思いがあります。

  • Rails のレールにはできるだけ乗りたいが、フロントエンド環境は分離したい
  • 新しいヘルパを導入する必要がある ( javascript_pack_tag など )
  • Webpacker の開発速度に依存してしまうのが不安

そこで、以上の問題を踏まえつつ、スムーズに移行できるような構成を考えてみました。

方針

  • Sprockets のヘルパーや digest 機構はそのまま使う
  • なるべく webpack で完結 (gulp や grunt のようなツール、rake タスクを書かない)
  • Font Awesome1 のような JS 以外のファイルを含むライブラリやフレームワークも webpack で管理

課題

Sprockets の digest 付きアセットファイルを扱う方法

以下の記事がとても詳しく参考になりました。

Sprockets再考 モダンなJSのエコシステムとRailsのより良い関係を探す

こちらの記事では gulp-rev-rails-manifest を使っていますが、gulp 及び gulp-rev の使用が前提です。今回は webpack で完結させたいので要件に合いませんでした。そこで同等の機能を提供する webpack-sprockets-rails-manifest-plugin という webpack のプラグインを利用することにしました。

設定例

以下の例では簡単のため、開発・プロダクション環境の分岐等の設定を省いています。
次のような設定をしています。

  • manifest 関連
    • webpack-sprockets-rails-manifest-plugin で webpack から config/sprockets-manifest.json に manifest ファイルを出力
    • webpack の出力した manifest ファイルを Sprockets が読み込めるようにパスを指定
  • webpack でビルドした生成物を public/assets/frontend/ 以下で配信
  • CSS や画像をビルドするための設定 (例として Font Awesome1)

ファイル構成

app/
  views/
    layouts/
      application.html.erb
config/
  development.yml
frontend/
  node_modules/
  src/
    app.js
  webpack.config.babel.js
  package.json
public/
  assets/
    frontend/
      node_modules/
      app-xxx.js (xxx は digest)

設定例

config/initializers/assets.rb
# webpack で public に出力したファイルを precompile 対象に含める
Rails.application.config.assets.precompile << /frontend\/.*\.(?:js|css)\z/

# Rails 3.x からデフォルトでは manifest ファイルのパスが動的に変わるようになった
# このままだと webpack から参照できないので静的なパスを指定
Rails.application.config.assets.manifest = Rails.root.join("config", "sprockets-manifest.json")
config/environments/development.rb
# Rails assets に manifest を読み込ませる
config.assets.debug = false
app/views/layouts/application.html.erb
...
<%= stylesheet_link_tag 'frontend/vendor', media: :all %>
...
<%= javascript_include_tag 'frontend/manifest', 'frontend/vendor', 'frontend/application' %>
...
frontend/webpack.config.babel.js
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import WebpackSprocketsRailsManifestPlugin from 'webpack-sprockets-rails-manifest-plugin';
import path from 'path';
import webpack from 'webpack';

// digest 値を末尾に付与
// Sprokcets の付与する digest とは形式が違うが、無理に揃える必要もないので簡単な方法で
const FILENAME = '[name]-[chunkhash]';

const extractCSS = new ExtractTextPlugin(`${FILENAME}.css`);

export default {
  entry: {
    'frontend/vendor': [
      'jquery',
      'jquery-ujs',
      'font-awesome/css/font-awesome.css'
    ],
    'frontend/app': [
      './src/app.js'
    ]
  },
  output: {
    path: path.resolve(__dirname, '../public/assets'),
    filename: `${FILENAME}.js`,
    chunkFilename: `${FILENAME}.js`
  },
  plugins: [
    extractCSS,
    new webpack.optimize.CommonsChunkPlugin({
      name: [
        'frontend/vendor',
        'frontend/manifest'
      ],
      minChunks: Infinity
    }),
    new WebpackSprocketsRailsManifestPlugin({
      manifestFile: '../../config/sprockets-manifest.json'
    })
  ],
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        'babel-loader'
      ]
    }, {
      test: /\.css$/,
      use: extractCSS.extract([
        'css-loader'
      ])
    }, {
      // ビルドしたアセットファイルを public 以下に配置
      test: /\.(jpeg|jpg|gif|png|svg|eot|woff|woff2|ttf|wav|mp3)$/,
      use: {
        loader: 'file-loader',
        options: {
          name: '[path][name].[hash].[ext]',
          outputPath: 'frontend/images/',
          publicPath: '/assets/frontend/images/'
        }
      }
    }]
  }
};

終わりに

public/assets/ 以下に node_modules の構造がそのまま反映されているのが少々格好悪いので直したいです。
css の読み込みも 'font-awesome/css/font-awesome.css' のように内部のファイル構造を知ってしまっているので、改善の余地がありそうです。

最後になりますが、この記事は Webpacker を批判するものではありません。
お互いの良い所を上手く取り込んで快適な開発ができるようになるといいですね。

参考文献