はじめに
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
ファイル内で以下のように変更できます。
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
で作成した初期状態から確認)
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>]
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 に
.
├── 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
としてコンパイルされます。
//= require test_a
//= require test_b
上の application.js は外部ファイルの test_a
と test_b
を読み込みます。
ファイルは Rails.application.config.assets.paths からの相対パスで指定します。
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);
}
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>
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);
}
;
以上のように、アセットパイプラインで require を使ってコンパイルされたファイルは 1 つにまとめられるため、var を使って変数を定義する場合は複数のファイル内で競合しないように注意する必要があります。
JavaScript, CSS で ruby の実行結果を使う方法
ファイルの拡張子に .erb
をつけることで ERB テンプレートとして処理した後に、JavaScript ファイルや CSS ファイルをコンパイルすることが出来ます。
以下の内容を書いた場合に、JavaScript ファイル内で環境変数の APP_NAME
を JavaScript の変数 app_name に代入することが出来ます。
var app_name = "<%= ENV['APP_NAME'] %>";
console.log(app_name);
ブラウザのコンソールに結果が表示されます。
Rails を使うことも出来ます。
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' %>
<!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.assets.js_compressor = :uglifier
例えば、以下のような JavaScript のファイルがあったとします。
//= require test_a
//= require test_b
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);
}
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);
}
すると、以下のようにコンパイル時にファイルが圧縮(最適化/難読化)されます。
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 />
タグが生成される
- JavaScript 用の
- アセットファイルへのパスはヘルパーメソッドがある
-
asset_path(<アセットファイル名>)
により取得できる
-
Webpacker の仕組み
移行に先駆けて Webpacker, Webpack の仕組みを紹介します。
(既に理解している場合は読み飛ばしてください)
Webpacker の動作概要
Webpacker は Webpack を 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.js
は public/packs/js/some_name-<Digest値>.js
となり、app/javascript/packs/some_name.scss
は public/packs/css/some_name-<Digest値>.css
となります。
ローダー
ローダーはアプリケーションが利用できるモジュールとなるようファイルを変換する Webpack の機能です。
Webpack は JavaScript と JSON ファイルしか処理できないため、それ以外の形式のファイルはローダーが処理します。
ここで JavaScript の import によりファイルを読み込みますが Webpack 独自の機能です。
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 がある場合にバンドルした結果を見てみます。
require('test_a');
require('test_b');
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);
}
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>
/******/ ({
/***/ "./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 で定義した変数は他のファイルから参照されていないことが分かります。
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' %>
<!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.yml
が extract_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
等でアクセスできる
- development 環境で
- javascripts/packs/application.js に書かれた require はファイル内容がそのまま展開されずに Webpack によりバンドルされるため var が競合しない
- HTML タグを埋め込むためのヘルパーメソッドがある
- JavaScript 用の
<%= javascript_pack_tag 'MODULE_NAME' %>
を使うと<script />
タグが生成される - CSS 用の
<%= stylesheet_pack_tag 'MODULE_NAME' %>
を使うと<link />
タグが生成される
- JavaScript 用の
- バンドルしたファイルへのパスはヘルパーメソッドがある
-
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.yml
の source_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 の設定が更新されます。参考
module.exports = {
test: /\.erb$/,
enforce: 'pre',
exclude: /node_modules/,
use: [{
loader: 'rails-erb-loader',
options: {
runner: (/^win/.test(process.platform) ? 'ruby ' : '') + 'bin/rails runner'
}
}]
}
: <snip>
const erb = require('./loaders/erb')
environment.loaders.prepend('erb', erb)
: <snip>
: <snip>
extensions:
- .erb
: <snip>
Vue.js ファイルを読み込めるようにする場合は rails webpacker:install:vue
を実行します。参考
エントリーポイントを洗い出す
Webpacker におけるエントリーポイントとなるファイルを洗い出します。
対象は Sprocket におけるコンパイル対象となる基準ファイルです。
- 標準のコンパイル対象ファイルである
application.js
,application.css
- 追加のコンパイル対象ファイルである
config/initializers/assets.rb
のRails.application.config.assets.precompile
に設定されているファイル
ファイル名がエントリーポイント名になります。
エントリーポイントの移行方法を決める
ファイル名がエントリーポイント名となるため、同じエントリーポイント名のモジュールが JavaScript のみ、又は Stylesheet のみである場合で移行方法を分けることにします。
- JavaScript と Stylesheet が同じ名前のファイルが存在する場合
- JavaScript をエントリーポイントにし、Stylesheet は JavaScript ファイルから import により読み込む
- JavaScript と Stylesheet が同じ名前のファイルが存在しない場合
- ファイルをエントリーポイントにする
尚、 config/webpacker.yml
が extract_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 が非表示になります。
gem "bootstrap", "~> 4.3"
gem "jquery-rails", "~> 4.3"
//= require jquery3
//= require popper
//= require bootstrap-sprockets
$(function() {
$('.alert').alert('close');
});
<!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>
<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">×</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 が非表示になります。
function close_alert() {
$('.alert').alert('close');
}
//= require alert
$(function() {
close_alert();
});
<!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>
<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">×</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.paths
に node_modules
ディレクトリを追加します。
(rails webpacker:install
を実行すると追加されます)
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 が非表示になります。
{
"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"
}
}
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery/dist/jquery
//= require bootstrap/dist/js/bootstrap
//= require alert
//= require_tree .
$(function() {
$('.alert').alert('close');
});
<!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>
<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">×</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 が非表示になります。
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
import 'bootstrap/dist/js/bootstrap';
$(function() {
$('.alert').alert('close');
});
import 'bootstrap/dist/js/bootstrap';
export function close_alert() {
$(function() {
$('.alert').alert('close');
});
}
//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require alert
//= require_tree .
Packs.application.close_alert();
<!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>
<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">×</span>
</button>
<strong>Holy guacamole!</strong> You should check in on some of those fields below.
</div>