147
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

Organization

Ruby on Rails で sprockets から Webpacker へ移行し、移行できないものは共存させる方法

はじめに

Rails6 以降は標準のモジュールバンドラとして Webpacker が使われるようになりました。
それより前に使っていた Sprockets は可能な限り Webpacker へ移行できればよいですが、Sprockets に依存しているモジュールは移行が出来ません。

そこで、Webpacker と Sprocket を共存させる方法について考えてみた結果を紹介します。

尚、自身の理解を深めるため Sprockets の動作詳細と Webpacker, Webpack の動作詳細を掲載しておりますので、既に理解している人で Sprockets から Webpacker へ移行する方法だけ知りたい人はSprocket を Webpacker に移行する方法から読んでください。

Sprockets の仕組み

移行に先駆けて Sprockets の仕組みを紹介します。
(既に理解している場合は読み飛ばしてください)

Sprockets の動作概要

Sprockets はデフォルトでは app/assets 配下にある JavaScript, CSS をそれぞれ 1 つのファイルにまとめ、まとめたファイルと images 配下にあるファイルを /public 配下にコピーします。

尚、Rails6 からは標準で Webpacker が使われるようになったため app/assets/javascripts ディレクトリは作成されず、Sprockets の対象パスからも外れたようです。
rails new の結果から確認

JavaScript と CSS をまとめる理由は、Rails アプリケーションにブラウザアクセスした時にサーバへのアクセス回数を減らすためです。

Sprockets がファイルをまとめるまでの工程はアセットパイプラインと呼ばれています。

アセットパイプラインは個別の処理における INPUT と OUTPUT を数珠つなぎにするパイプライン処理を指します。

アセットパイプラインが処理をする対象とするファイルは Rails.application.config.assets.paths から探索されます。
この設定は config/initializers/assets.rb ファイル内で以下のように変更できます。

config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('lib/my_javascripts')

Sprockets の動作詳細

Sprockets は sprockets, sprockets-rails gem により実装されています。

Sprockets による JavaScript, CSS のコンパイル

アセットパイプラインの処理を実行することコンパイルを呼びます。
これは development 環境では動的に行われ、production 環境では事前に rails assets:precompile で実行される処理を指します。

コンパイル対象ファイルは app/assets/javascripts/application.js, app/assets/stylesheets/application.css がマッチするよう次のとおり Proc が設定されています。

標準のコンパイル対象ファイル
Rails.application.config.assets.precompile
=> [#<Proc:0x0000565300041510@/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/sprockets-rails-3.2.1/lib/sprockets/railtie.rb:84 (lambda)>, /(?:\/|\\|\A)application\.(css|js)$/]

コンパイル対象のファイルはデフォルトで次のとおりです。(rails new で作成した初期状態から確認)

コンパイル対象のファイル(Rails5の場合)
irb(main):001:0> pp Rails.application.config.assets.paths
["/home/vagrant/work/rails/railssample5/app/assets/config",
 "/home/vagrant/work/rails/railssample5/app/assets/images",
 "/home/vagrant/work/rails/railssample5/app/assets/javascripts",
 "/home/vagrant/work/rails/railssample5/app/assets/stylesheets",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/coffee-rails-4.2.2/lib/assets/javascripts",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actioncable-5.2.3/lib/assets/compiled",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/activestorage-5.2.3/app/assets/javascripts",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actionview-5.2.3/lib/assets/compiled",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/turbolinks-source-5.2.0/lib/assets/javascripts",
 #<Pathname:/home/vagrant/work/rails/railssample5/node_modules>]
=> ["/home/vagrant/work/rails/railssample5/app/assets/config", "/home/vagrant/work/rails/railssample5/app/assets/images", "/home/vagrant/work/rails/railssample5/app/assets/javascripts", "/home/vagrant/work/rails/railssample5/app/assets/stylesheets", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/coffee-rails-4.2.2/lib/assets/javascripts", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actioncable-5.2.3/lib/assets/compiled", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/activestorage-5.2.3/app/assets/javascripts", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actionview-5.2.3/lib/assets/compiled", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/turbolinks-source-5.2.0/lib/assets/javascripts", #<Pathname:/home/vagrant/work/rails/railssample5/node_modules>]
コンパイル対象のファイル(Rails6の場合)
irb(main):002:0> pp Rails.application.config.assets.paths
["/PATH_TO_YOUR_APPLICATION/app/assets/config",
 "/PATH_TO_YOUR_APPLICATION/app/assets/images",
 "/PATH_TO_YOUR_APPLICATION/app/assets/stylesheets",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actioncable-6.0.0/app/assets/javascripts",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/activestorage-6.0.0/app/assets/javascripts",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actionview-6.0.0/lib/assets/compiled",
 "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/turbolinks-source-5.2.0/lib/assets/javascripts",
 #<Pathname:/PATH_TO_YOUR_APPLICATION/node_modules>]
=> ["/PATH_TO_YOUR_APPLICATION/app/assets/config", "/PATH_TO_YOUR_APPLICATION/app/assets/images", "/PATH_TO_YOUR_APPLICATION/app/assets/stylesheets", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actioncable-6.0.0/app/assets/javascripts", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/activestorage-6.0.0/app/assets/javascripts", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/actionview-6.0.0/lib/assets/compiled", "/home/vagrant/.rbenv/versions/2.5.5/lib/ruby/gems/2.5.0/gems/turbolinks-source-5.2.0/lib/assets/javascripts", #<Pathname:/PATH_TO_YOUR_APPLICATION/node_modules>]

基本的に、アプリケーションに JavaScript や CSS を追加したい場合は app/assets/javascripts, app/assets/stylesheets 配下にファイルを作成して、application.js, application.css から読み込むように設定します。

Sprockets で外部の JavaScript や CSS ファイルを読み込むためにはディレクティブを使います。
ディレクティブは JavaScript として解釈されないようにコメントとして記載します。

ディレクティブの種類には次のものがあり、1行に1ディレクティブを指定します。(詳細は Sprockets を参考にして下さい)

  • require - 指定されたパスの外部ファイル内容を展開する
  • require_self - ファイル内容(コメント内のディレクティブは除く)を指定した位置に展開する
  • require_directory - 指定したディレクトリ直下にある全てのファイル内容を展開する(ディレクトリはディレクティブを記載したファイルからの相対パスで指定し、ディレクトリ内のファイルはファイル名昇順で読み込まれる)
  • require_tree - 指定したディレクトリ配下のサブディレクトリを含む全てのファイル内容を展開する(ディレクトリ直下のファイルをファイル名昇順で読み込み、その後にディレクトリがあれば再帰的に読み込みます)
    • 以下のディレクトリ構造の場合、 application.js に //= require_tree . を指定した場合、a.js -> b.js -> z.js -> c.js の順で読み込まれる
.
├── application.js
├── dir_a
│   ├── a.js
│   ├── b.js
│   └── dia_z
│       └── z.js
└── dir_c
    └── c.js
  • link - 指定したパスをコンパイルするがファイル内容を展開はしない
  • link_directory - 指定したディレクトリ直下のファイルをコンパイルするがファイル内容を展開はしない(require_directoryと同じ順序)
  • link_tree - 指定したディレクトリ配下のサブディレクトリを含む全てのファイル内容をコンパイルするが展開しない(require_treeと同じ順序)
  • depend_on - 指定したファイルが更新された場合に再コンパイルする
  • depend_on_asset - 指定したディレクトリ配下の全ファイルに書かれた、指定したディレクティブに従う
  • stub - 指定したパスを無視する

例えば、次のような application.js があった場合にコンパイルされた結果のファイルは public/assets/application-<Digest値>.js としてコンパイルされます。

app/assets/javascripts/application.js
//= require test_a
//= require test_b

上の application.js は外部ファイルの test_atest_b を読み込みます。
ファイルは Rails.application.config.assets.paths からの相対パスで指定します。

app/assets/javascripts/test_a.js
console.log('test_a.js');
if (typeof(test_a) === "undefined") {
  var test_a = 'var test_a';
  console.log(test_a);
}
if (typeof(test_b) !== "undefined") {
  console.log(test_b);
}
app/assets/javascripts/test_b.js
console.log('test_b.js');
if (typeof(test_a) !== "undefined") {
  console.log(test_a);
}
if (typeof(test_b) === "undefined") {
  var test_b = 'var test_b';
  console.log(test_b);
}
$ bin/rails assets:precompile
  : <snip>
I, [2019-09-07T16:13:18.790631 #26248]  INFO -- : Writing /PATH_TO_YOUR_APPLICATION/public/assets/application-778627a6562acff4de6fa0ad315421f941a1bdcd22fe7ac6c17a2e24299e75e9.js
  : <snip>
public/assets/application-778627a6562acff4de6fa0ad315421f941a1bdcd22fe7ac6c17a2e24299e75e9.js
if (typeof(test_a) === "undefined") {
  var test_a = 'var test_a';
  console.log(test_a);
}
if (typeof(test_b) !== "undefined") {
  console.log(test_b);
}
;
console.log('test_b.js');
if (typeof(test_a) !== "undefined") {
  console.log(test_a);
}
if (typeof(test_b) === "undefined") {
  var test_b = 'var test_b';
  console.log(test_b);
}
;

ブラウザのコンソールログ
image.png

以上のように、アセットパイプラインで require を使ってコンパイルされたファイルは 1 つにまとめられるため、var を使って変数を定義する場合は複数のファイル内で競合しないように注意する必要があります。

JavaScript, CSS で ruby の実行結果を使う方法

ファイルの拡張子に .erb をつけることで ERB テンプレートとして処理した後に、JavaScript ファイルや CSS ファイルをコンパイルすることが出来ます。

以下の内容を書いた場合に、JavaScript ファイル内で環境変数の APP_NAME を JavaScript の変数 app_name に代入することが出来ます。

app/application/javascripts/app_name.js.erb
var app_name = "<%= ENV['APP_NAME'] %>";
console.log(app_name);

ブラウザのコンソールに結果が表示されます。

Rails を使うことも出来ます。

app/application/javascripts/app_name.js.erb
var first_asset_path = "<%= Rails.application.config.assets.paths.first %>";
console.log(first_asset_path);

アセットパイプライン対象ディレクトリのパスの内最初の 1 つがブラウザのコンソールに表示されます。

Sprockets がコンパイルした JavaScript, CSS ファイルを Rails から読み込む方法

Sprockets がコンパイルしたファイルを Rails から使うためのヘルパーメソッドが sprockets-rails gem により定義されています。
これにより View ファイルの ERB テンプレート内で <%= javascript_include_tag 'application' %> を指定することでコンパイルされた application.js をページ内に読み込む HTML タグを埋め込むことが出来ます。

  • コンパイルした application.js を読み込む方法
    • <%= javascript_include_tag 'application' %>
  • コンパイルした application.css を読み込む方法
    • <%= stylesheet_link_tag 'application' %>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>App</title>
  : <snip>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application' %>
  </head>

  <body>
    <div>
      <%= yield %>
    </div>
  </body>
</html>
/ページのソースコード
<html>
  <head>
    <title>App</title>
  : <snip>
    <link rel="stylesheet" media="all" href="/assets/application.self-f0d704deea029cf000697e2c0181ec173a1b474645466ed843eb5ee7bb215794.css?body=1" data-turbolinks-track="reload" />
<script src="/assets/application.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.js?body=1"></script>
  : <snip>
  </head>
  : <snip>

ここで、Sprockets がコンパイルしたファイルのパスはヘルパーメソッド asset_path() により解決できます。

コンパイル処理を設定する方法

コンパイル時にファイルを圧縮することが出来ます。

圧縮するには config/initializers/assets.rb で js_compressor, css_compressor を指定します。

圧縮するメリットはファイルサイズが減るため通信時間が減ることです。

JavaScript を圧縮する uglifier gem があり、Rails5 までは標準でバンドルされます。

config/environments/production.rb
config.assets.js_compressor = :uglifier

例えば、以下のような JavaScript のファイルがあったとします。

app/assets/javascripts/some_name.js
//= require test_a
//= require test_b
app/assets/javascripts/test_a.js
console.log('test_a.js');
if (typeof(test_a) === "undefined") {
  var test_a = 'var test_a';
  console.log(test_a);
}
if (typeof(test_b) !== "undefined") {
  console.log(test_b);
}
app/assets/javascripts/test_b.js
console.log('test_b.js');
if (typeof(test_a) !== "undefined") {
  console.log(test_a);
}
if (typeof(test_b) === "undefined") {
  var test_b = 'var test_b';
  console.log(test_b);
}

すると、以下のようにコンパイル時にファイルが圧縮(最適化/難読化)されます。

public/assets/some_name-<Digest値>.js
if (console.log("test_a.js"), void 0 === test_a) {
  var test_a = "var test_a";
  console.log(test_a)
}
if (void 0 !== test_b && console.log(test_b), console.log("test_b.js"), void 0 !== test_a && console.log(test_a), void 0 === test_b) {
  var test_b = "var test_b";
  console.log(test_b)
}

また CSS を圧縮する yui-compressor, sass-rails gem があり、これらを使う場合は css_compressor に設定します。

Sprockets の動作まとめ

  • app/assets ファイル内の javascripts/application.js, stylesheets/application.css に書かれたディレクティブに従って 1 つのファイルにコンパイルする
  • コンパイルされたファイルと app/assets/images 配下のファイルは静的ファイルとしてブラウザからアクセスできるパスに配置される
    • config.assets.compile = true を設定して動作している場合(development 環境で rails s した場合の標準設定)、コンパイルしたファイルは tmp/cache 配下にキャッシュされる
      • /assets/application.js 等でアクセスできる
    • rails assets:precompile 実行(production モード等)時は public 配下にコピーされる
      • /assets/application-<Digest値>.js 等でアクセスできる
  • javascripts/application.js に書かれた require ディレクティブはファイル内容を展開するため var が競合しないように注意する必要がある
  • HTML タグを埋め込むためのヘルパーメソッドがある
    • JavaScript 用の <%= javascript_include_tag 'MODULE_NAME' %> を使うと <script /> タグが生成される
    • CSS 用の <%= stylesheet_link_tag 'MODULE_NAME' %> を使うと <link /> タグが生成される
  • アセットファイルへのパスはヘルパーメソッドがある
    • asset_path(<アセットファイル名>) により取得できる

Webpacker の仕組み

移行に先駆けて Webpacker, Webpack の仕組みを紹介します。
(既に理解している場合は読み飛ばしてください)

Webpacker の動作概要

WebpackerWebpack を rails で使うラッパーです。

config/webpack/ 配下に RAILS_ENV 毎の設定ファイルがあります。

  • NODE_ENV には production, development, test があります
  • config/webpack/*.js にて、デフォルトの NODE_ENV が指定されています
  • RAILS_ENV に対応する config/webpack/*.js が存在しない場合は NODE_ENV=production に fallback します

Webpacker でモジュールをバンドルするには rails webpacker:compile または bin/webpack を使います。

バンドルされたファイルは public/packs 配下にコピーされます。

バンドルする対象のファイルは app/javascript 配下のファイルです。

バンドルする単位をエントリーポイントと呼び、エントリーポイントとなるファイルは app/javascript/packs 配下のファイルです。
JavaScript, CSS のどちらであっても app/javascript/packs 配下に配置することでエントリーポイントとすることが出来ます。
エントリーポイントとなるファイルに JavaScript は import, CSS は @import を記載するとそれらがエントリーポイントのファイル名としてバンドルされます。

Webpack 詳細

Webpack はモダンな JavaScript アプリケーションで使われている JavaScript, CSS, images のバンドラーです。

Webpack を理解するために「エントリー」「アウトプット」「ローダー」「プラグイン」「モード」について説明します。
(参考)

エントリー

エントリーポイントは webpack が内部的に持つ依存関係グラフを構築開始するポイントを指します。

このポイントとなるファイルを視点として、import による依存関係を辿っていき 1 つのファイルを生成します。

  • アウトプット

Webpacker がバンドルしたファイルは public/packs 配下にコピーされます。
ファイル名はエントリーポイントとなるファイルと同じ名前となります。

例: app/javascript/packs/some_name.jspublic/packs/js/some_name-<Digest値>.js となり、app/javascript/packs/some_name.scsspublic/packs/css/some_name-<Digest値>.css となります。

ローダー

ローダーはアプリケーションが利用できるモジュールとなるようファイルを変換する Webpack の機能です。

Webpack は JavaScript と JSON ファイルしか処理できないため、それ以外の形式のファイルはローダーが処理します。

ここで JavaScript の import によりファイルを読み込みますが Webpack 独自の機能です。

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

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};
import 'some_name.txt';  // Webpack some_name.txt

some_name.txt がエントリーポイントの依存関係に追加されます。

プラグイン

プラグインはバンドルを最適化し、アセットを管理し、環境変数の注入などを行います。

例えば HTML をビルドするプラグインがあります。

モード

モードは Webpack のビルトインされた最適化機能を設定する値です。

モードには development, production, none があり、標準のモードは production です。

ブラウザ互換性

Webpack は ES5-complient である全てのブラウザに対応しています。

Webpacker の動作詳細

Webpacker は webpacker gem により実装されています。

Webpacker による JavaScript, CSS のバンドル

JavaScript, CSS, Image は Webpack によりバンドルされます。

例えば次のような JavaScript がある場合にバンドルした結果を見てみます。

app/javascript/packs/some_name.js
require('test_a');
require('test_b');
app/javascript/test_a.js
console.log('test_a.js');
if (typeof(test_a) === "undefined") {
  var test_a = 'var test_a';
  console.log(test_a);
}
if (typeof(test_b) !== "undefined") {
  console.log(test_b);
}
app/javascript/test_b.js
console.log('test_b.js');
if (typeof(test_a) !== "undefined") {
  console.log(test_a);
}
if (typeof(test_b) === "undefined") {
  var test_b = 'var test_b';
  console.log(test_b);
}

バンドルした結果は次のとおりです。

public/packs/js/some_name-9c38fbd746f19f54f15e.js
  : <snip>
/******/ ({

/***/ "./app/javascript/packs/some_name.js":
/*!*******************************************!*\
  !*** ./app/javascript/packs/some_name.js ***!
  \*******************************************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

__webpack_require__(/*! test_a */ "./app/javascript/test_a.js");

__webpack_require__(/*! test_b */ "./app/javascript/test_b.js");

/***/ }),

/***/ "./app/javascript/test_a.js":
/*!**********************************!*\
  !*** ./app/javascript/test_a.js ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

console.log('test_a.js');

if (typeof test_a === "undefined") {
  var test_a = 'var test_a';
  console.log(test_a);
}

if (typeof test_b !== "undefined") {
  console.log(test_b);
}

/***/ }),

/***/ "./app/javascript/test_b.js":
/*!**********************************!*\
  !*** ./app/javascript/test_b.js ***!
  \**********************************/
/*! no static exports found */
/***/ (function(module, exports) {

console.log('test_b.js');

if (typeof test_a !== "undefined") {
  console.log(test_a);
}

if (typeof test_b === "undefined") {
  var test_b = 'var test_b';
  console.log(test_b);
}

/***/ })

/******/ });
  : <snip>

__webpack_require__ は Webpack が定義する関数です。
このように require したファイルは Webpack のモジュールとして組み込まれた上で 1 つのファイルとしてまとめられます。

ブラウザで実行した結果を見ると分かるように var で定義した変数は他のファイルから参照されていないことが分かります。

image.png

Webpacker がバンドルした JavaScript, CSS ファイルを Rails から読み込む方法

Webpacker がコンパイルしたファイルを Rails から使うためのヘルパーメソッドが定義されています。
これにより View ファイルの ERB テンプレート内で <%= javascript_pack_tag 'application' %> を指定することでバンドルされた application.js をページ内に読み込む HTML タグを埋め込むことが出来ます。

  • バンドルした application.js を読み込む方法
    • <%= javascript_pack_tag 'application' %>
  • バンドルした application.css を読み込む方法
    • <%= stylesheet_pack_tag 'application' %>
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Rails60SampleApp</title>
  : <snip>
    <%= stylesheet_pack_tag 'rails6_0_sample_app', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'rails6_0_sample_app', 'data-turbolinks-track': 'reload' %>
  : <snip>
  </head>

  <body>
    <%= render partial: 'layouts/navbar' %>
    <div class="container-fluid p-60">
      <%= yield %>
    </div>
  </body>
</html>
/ページのソースコード
<html>
  <head>
    <title>App</title>
  : <snip>
    <link rel="stylesheet" media="all" href="/packs/css/rails6_0_sample_app_0-0f7904d3.css" data-turbolinks-track="reload" />
    <script src="/packs/js/rails6_0_sample_app-351f96ac6da7acaa06c3.js" data-turbolinks-track="reload"></script>
  : <snip>
  </head>
  : <snip>

ここで、Sprockets がコンパイルしたファイルのパスはヘルパーメソッド asset_path() により解決できます。

尚、 config/webpacker.ymlextract_css: true である場合だけ stylesheet_pack_tag が <link /> タグを出力するようです。(stylesheet_pack_tag not working in webpacker 4?? #2059)

Webpacker の動作まとめ

  • app/javascript/packs ファイル内がエントリーファイルとなり、Webpack によりそれぞれ 1 つのファイルにバンドルされる
  • バンドルされたファイルと app/javascript/images 等に配置されたファイルは静的ファイルとしてブラウザからアクセスできるパスに配置される
    • development 環境で rails s した場合は必要に応じてバンドル処理が実行される
    • 明示的に rails webpacker:compile を実行(rails assets:precompile を実行すると続いて実行される)
    • /packs/application-<Digest値>.js 等でアクセスできる
  • javascripts/packs/application.js に書かれた require はファイル内容がそのまま展開されずに Webpack によりバンドルされるため var が競合しない
  • HTML タグを埋め込むためのヘルパーメソッドがある
    • JavaScript 用の <%= javascript_pack_tag 'MODULE_NAME' %> を使うと <script /> タグが生成される
    • CSS 用の <%= stylesheet_pack_tag 'MODULE_NAME' %> を使うと <link /> タグが生成される
  • バンドルしたファイルへのパスはヘルパーメソッドがある
    • asset_pack_path(<アセットファイル名>) により取得できる

Sprocket を Webpacker に移行する方法

基本方針

これまで見てきた Sprocket と Webpacker の動作から移行する際の基本方針を考えてみます。

尚、全て Webpacker に移行できない前提とします。

  • Sprocket におけるコンパイル対象となる基準ファイルを Webpacker におけるエントリーポイントにする
  • 基準ファイルが読み込むファイルは Webpacker のエントリーポイントにしない
  • Sprocket から Webpacker に移行したファイルを参照するためにアセットパスを追加する
  • Webpacker から Sprocket に残ったファイルを参照するための特別な方法はない (Sprockets にコンパイルされた内容はグローバル空間に展開されるため)

もし完全に Webpacker に移行できる場合は以下の gem は Webpack により置き換わるので不要となるでしょう。

  • closure-compiler
  • uglifier
  • yui-compressor
  • sass-rails

ディレクトリ構成

移行した後の Sprockets 関係ファイルと Webpacker 関係ファイルのディレクトリ構成は次のとおりです。

移行後のディレクトリ構成
# Sprockets 関係ファイル
app/assets
├── images
│   :   <何かファイル>
├── javascripts
│   :   <エントリーポイント以外のJavaScriptファイル>
│   └── application.js
└── stylesheets
    :   <エントリーポイント以外のCSSファイル>
    └── application.css

# Webpacker 関係ファイル
app/javascript
├── packs
│   :   <エントリーポイントとなるJavaScript/CSSファイル>
├── src
│   :   <エントリーポイント以外のJavaScript/CSSファイル>
├── images
│   :   <何かファイル>
└── node_modules
    :   <WebpackerでインストールしたJavaScript/CSSファイル>

基本方針はなるべく Webpacker の default 設定のままにし、ファイルを配置する構成は Webpacker の README.md に書かれた内容に従うものです。

ディレクトリ構成を変更したい場合の設定方法

Webpacker(Webpack) の設定ファイル config/webpacker.ymlsource_path, source_entry_path を変更することで、 app/javascripts/packs 以外のディレクトリをエントリーポイントにすることも出来ます。

default: &default
  source_path: app/javascript
  source_entry_path: packs
  : <snip>

Webpacker で取り扱えるファイルの拡張子を増やす方法

Webpack のローダを設定することにより .js.erb のような ERB テンプレートや、.vue のような Vue.js ファイルを読み込むことも出来ます。

ERB テンプレートを読み込めるようにする場合は rails webpacker:install:erb を実行すれば Webpack の loader がインストールされ、Webpack の設定が更新されます。参考

config/webpack/loaders/erb.js
module.exports = {
  test: /\.erb$/,
  enforce: 'pre',
  exclude: /node_modules/,
  use: [{
    loader: 'rails-erb-loader',
    options: {
      runner: (/^win/.test(process.platform) ? 'ruby ' : '') + 'bin/rails runner'
    }
  }]
}
config/webpack/environment.js
  : <snip>
const erb =  require('./loaders/erb')
environment.loaders.prepend('erb', erb)
  : <snip>
config/webpacker.yml
  : <snip>
  extensions:
    - .erb
  : <snip>

Vue.js ファイルを読み込めるようにする場合は rails webpacker:install:vue を実行します。参考

エントリーポイントを洗い出す

Webpacker におけるエントリーポイントとなるファイルを洗い出します。

対象は Sprocket におけるコンパイル対象となる基準ファイルです。

  • 標準のコンパイル対象ファイルである application.js, application.css
  • 追加のコンパイル対象ファイルである config/initializers/assets.rbRails.application.config.assets.precompile に設定されているファイル

ファイル名がエントリーポイント名になります。

エントリーポイントの移行方法を決める

ファイル名がエントリーポイント名となるため、同じエントリーポイント名のモジュールが JavaScript のみ、又は Stylesheet のみである場合で移行方法を分けることにします。

  • JavaScript と Stylesheet が同じ名前のファイルが存在する場合
    • JavaScript をエントリーポイントにし、Stylesheet は JavaScript ファイルから import により読み込む
  • JavaScript と Stylesheet が同じ名前のファイルが存在しない場合
    • ファイルをエントリーポイントにする

尚、 config/webpacker.ymlextract_css: true である場合だけ stylesheet_pack_tag ヘルパーが <link /> タグを出力するようなので、CSS をエントリーポイントにした場合は extract_css: true にしましょう。(stylesheet_pack_tag not working in webpacker 4?? #2059)

Webpacker と Sprockets を共存する

Webpacker へ移行が出来なかったファイルから Sprockets のファイルを読み込む方法と、その逆となる方法を紹介します。

Webpacker のモジュールから Sprockets のモジュールを呼び出す

Sprockets を使って読み込む JavaScript はグローバルに展開されています。

そのため、Webpacker で管理する JavaScript からメソッド等を呼び出すことが可能です。

gem でインストールしたモジュールを呼び出す

assets/javascript/application.js で使用する gem を require し、app/views/layouts/application.html.erb 等で <%= javascript_include_tag 'application' %> を使って読み込みます。

例えば、Sprockets で bootstrap をインストールしていて、Webpacker 側の JavaScript ファイルで操作をしたい場合は以下のように $ を使って操作できます。

尚、正常に動作すると app/views/users/show.html.erb に書かれた alert クラスの div が非表示になります。

Gemfile
gem "bootstrap", "~> 4.3"
gem "jquery-rails", "~> 4.3"
app/assets/javascripts/application.js
//= require jquery3
//= require popper
//= require bootstrap-sprockets
app/javascript/packs/application.js
$(function() {
  $('.alert').alert('close');
});
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Rails5Sample</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
app/views/users/show.html.erb(users/show.html.erbである必要はありません)
<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
  <strong>Holy guacamole!</strong> You should check in on some of those fields below.
</div>

自身で追加したモジュールを呼び出す

こちらも gem の時と同じく、
assets/javascript/application.js で使用する gem を require し、app/views/layouts/application.html.erb 等で <%= javascript_include_tag 'application' %> を使って読み込みます。

例えば、Sprockets で bootstrap をインストールしていて、Webpacker 側の JavaScript ファイルで操作をしたい場合は以下のように $ を使って操作できます。

尚、正常に動作すると app/views/users/show.html.erb に書かれた alert クラスの div が非表示になります。

app/assets/javascripts/alert.js
function close_alert() {
  $('.alert').alert('close');
}
app/assets/javascripts/application.js
//= require alert
app/javascript/packs/application.js
$(function() {
  close_alert();
});
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Rails5Sample</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
app/views/users/show.html.erb(users/show.html.erbである必要はありません)
<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
  <strong>Holy guacamole!</strong> You should check in on some of those fields below.
</div>

Sprockets のモジュールから Webpacker のモジュールを呼び出す

Sprockets のモジュールから Webpacker のモジュールを呼び出すにはいくつかやり方がありそうですが、ここではアセットパイプラインのコンパイル対象に Webpacker でインストールしたモジュールを追加する方法を紹介します。

yarn でインストールしたモジュールを呼び出す

Rails.application.config.assets.pathsnode_modules ディレクトリを追加します。
(rails webpacker:install を実行すると追加されます)

config/initializers/assets.rb
Rails.application.config.assets.paths << Rails.root.join('node_modules')

後は、Sprockets の動作に従って assets/javascripts/application.js に require することで読み込めます。

例えば、Webpacker で bootstrap をインストールしていて、Sprockets 側の JavaScript ファイルで操作をしたい場合は以下のようにします。

尚、正常に動作すると app/views/users/show.html.erb に書かれた alert クラスの div が非表示になります。

package.json
{
  "name": "rails5_sample",
  "private": true,
  "dependencies": {
    "@rails/webpacker": "^4.0.7",
    "bootstrap": "^4.3.1",
    "jquery": "^3.4.1",
    "popper.js": "^1.15.0"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.8.0"
  }
}
app/assets/javascripts/application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks

//= require jquery/dist/jquery
//= require bootstrap/dist/js/bootstrap
//= require alert

//= require_tree .
app/assets/javascripts/alert.js
$(function() {
  $('.alert').alert('close');
});
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Rails5Sample</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
app/views/users/show.html.erb(users/show.html.erbである必要はありません)
<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
  <strong>Holy guacamole!</strong> You should check in on some of those fields below.
</div>

自身で追加したモジュールを呼び出す

Webpacker でバンドルしたモジュールは import して使う必要がありますが、Sprocket で管理している application.js 内で import 'some/files.js'; のように指定しても次のエラーが発生して import 出来ないようです。

SyntaxError: import declarations may only appear at top level of a module

そこで Webpack の output.library 設定を使ってグローバル空間に export します。(参考)

例えば、Webpacker で bootstrap をインストールしていて、Sprockets 側の JavaScript ファイルで操作をしたい場合は以下のようにします。

尚、正常に動作すると app/views/users/show.html.erb に書かれた alert クラスの div が非表示になります。

config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const webpack = require('webpack')

// Add an ProvidePlugin
environment.plugins.append('Provide',  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
    Popper: ['popper.js', 'default']
  })
)

const config = environment.toWebpackConfig()

config.resolve.alias = {
  jquery: "jquery/src/jquery"
}

environment.config.set('output.library', ['Packs', '[name]'])

module.exports = environment
app/javascript/packs/alert.js
import 'bootstrap/dist/js/bootstrap';

$(function() {
  $('.alert').alert('close');
});
app/javascript/packs/application.js
import 'bootstrap/dist/js/bootstrap';

export function close_alert() {
  $(function() {
    $('.alert').alert('close');
  });
}
app/assets/javascripts/application.js
//= require rails-ujs
//= require activestorage
//= require turbolinks

//= require alert

//= require_tree .
app/assets/javascripts/alert.js
Packs.application.close_alert();
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>Rails5Sample</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> // javascript_include_tag よりも先に指定しましょう
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>
app/views/users/show.html.erb(users/show.html.erbである必要はありません)
<div class="alert alert-warning alert-dismissible fade show" role="alert">
  <button type="button" class="close" data-dismiss="alert" aria-label="Close">
    <span aria-hidden="true">&times;</span>
  </button>
  <strong>Holy guacamole!</strong> You should check in on some of those fields below.
</div>
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
147
Help us understand the problem. What are the problem?