🕸️

Rails6, Webpack(not webpacker)でJSを最低限動かすまで

2021/01/15に公開

Rails6で、フロントエンド周りをデフォルトのWebpackerではなく、Webpackを使おうと思ったので書いた記事です。

使用したバージョン

ruby2.6.3
Rails6.1.0
node14.15.4
webpack5.12.2

初期プロジェクト作成

rails new . --skip-javascript --skip-sprockets --database=mysql

--skip-javascript --skip-sprocketsオプションをつけることでsass-rails, webpacker, turbolinksあたりをインストールしなくなります。
(後にCSSもwebpackでバンドルしようと思っているのでsprocketsもskip)

プロジェクトの中にfrontend ディレクトリを作っておきます。

以下のコマンドはすべてfrontend ディレクトリの中で行います。

Webpackでファイルをバンドルできるようにする

Webpackをインストールします。
https://webpack.js.org/guides/getting-started/

npm init

質問に適当に答えていき、package.json を生成します。

まずは最低限webpackを動かせる環境を作ります。

webpackをインストールします。

npm install webpack webpack-cli --save-dev

webpackでバンドルしたいファイルを用意します。
frontend/src/index.js を作成します。

// frontend/src/index.js
console.count('hello, world')

設定ファイルfrontend/webpack.config.jsを作成します。

// frontend/webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js', // バンドル元となるファイル
  output: { // バンドルされたファイルの出力先とファイル名
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

webpackコマンドを使えるようにfrontend/package.jsonを編集します。

{
  "name": "frontend",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "dev": "webpack" // npm runでwebpackを使えるようにする。
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^5.12.2",
    "webpack-cli": "^4.3.1"
  }
}

以下のコマンドでwebpackでバンドルできます。

npm run dev

するとfrontend/dist/が生成され中にmain.jsが生成されます。

// frontend/dist/main.js
console.count("hello, world");

ここまでで最低限webpackを動かす環境ができました。

バンドルされたファイルをRailsで読み込めるようにする

やることは

  1. バンドルされたファイルの出力先をpublicディレクトリにする
  2. キャッシュ対策として、バンドルされるファイルにhashを付与する
  3. manifest.jsonを生成する
  4. scriptタグを生成するヘルパー関数を作成する
    です。

バンドルされたファイルの出力先をpublicディレクトリにする

出力されたファイルをブラウザから読み込めるようにするためです。

// frontend/webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
-    path: path.resolve(__dirname, 'dist'),
+    path: path.resolve(__dirname, '../public/packs'),
  },
};

webpackによるファイルの置き場だとわかるようにpublic/packsを作ってその中に出力するようにします。

キャッシュ対策としてバンドルされるファイルにhashを付与する

開発していて、バンドルされて生成されるファイル名が常に同じだとキャッシュが効いて変更が反映されない場合があります。
そのためファイル名にhashを付与し元のファイルに変更があると違うファイル名になるようにすることで、変更を確実に反映させます。

https://webpack.js.org/guides/caching/#output-filenames

// frontend/webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
-    filename: 'main.js',
+    filename: 'main-[contenthash].js',
    path: path.resolve(__dirname, '../public/packs'),
  },
};

こうすることでバンドルしたときにmain-98e4726ae81fac80db84.jsと、hashがついた形で出力されるようになります。

manifest.jsonを生成する

これを使います。
https://github.com/shellscape/webpack-manifest-plugin
例にあるようにこのようなjsonファイルが一緒に生成されます。

{
  "dist/batman.js": "dist/batman.1234567890.js",
  "dist/joker.js": "dist/joker.0987654321.js"
}

このファイルによってRailsがscriptタグを生成するときの埋め込むべきファイル名を知ることができます。

npm install webpack-manifest-plugin --save-dev
// frontend/webpack.config.json
const path = require('path');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main-[contenthash].js',
    path: path.resolve(__dirname, '../public/packs'),
  },
  plugins: [
    new WebpackManifestPlugin({
      publicPath: '/packs/' // /packs/main-xxxxxxxx.jsという風にpacks以下に生成される
    })
  ]
};
{
  "main.js": "/packs/main-98e4726ae81fac80db84.js"
}

scriptタグを生成するヘルパー関数を作成する

・manifest.jsonの中身を取得
・ハッシュを除いたファイル名をキーにして対応するファイル名を取得
・javascript_include_tagを発行

# helpers/webpack_bundle_helper.rb
module WebpackBundleHelper
  class BundleNotFound < StandardError; end

  def javascript_webpack_bundle_tag(entry, **options)
    path = asset_bundle_path("#{entry}.js")
    javascript_include_tag(path, **options)
  end

  private

  MANIFEST_FILE = 'manifest.json'.freeze

  def asset_bundle_path(entry, **options)
    raise BundleNotFound, "Could not find bundle with name #{entry}" unless manifest.key? entry
    asset_path(manifest.fetch(entry), **options)
  end

  def manifest
    @manifest ||= JSON.parse(File.read(manifest_path))
  end

  def manifest_path
    Rails.root.join('public/packs', MANIFEST_FILE)
  end
end

作成したヘルパーをviewにセットします。

# views/layouts/application.html.erb

  <%= javascript_webpack_bundle_tag('main') %>

ブラウザで確認してみると以下のようにscriptタグが埋め込まれていて、無事JSを読み込むことができました。

<head>
	// 省略
  <script src="/packs/main-98e4726ae81fac80db84.js"></script>
	//省略
</head>

余談

本番環境だと以下がデフォルトだとfalseなので、public/以下にアクセスできません。
trueになるようにしておく必要があります。public以下はNginxで配信するという場合は必要ないですが。

# config/environments/producition.rb
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?

また、webpackコマンドを実行したときに以下の警告が表示されるかと思います。

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

modeを設定していないためです。
webpack.config.jsにmodeを設定するか、

// paclage.json
"scripts": {
    "dev": "webpack --mode=development",

コマンドのオプションとして渡す方法があるかと思います。

modeのちがい。
productionの場合ははとにかくバンドルされるファイルサイズを小さくします。動作に必要なものだけ出力される感じです。
対してdevelopmentだと動作に必要なものに加えてデバッグしやすいようにその他の情報も出力されます。

Discussion