Ruby on Rails のアセットパイプラインの挙動を環境ごとに学ぶ

アセットパイプラインとは、Ruby on RailsにおいてJavaScriptファイル、スタイルシート、画像等(これらを総称してアセットと呼ぶ)を管理する仕組みのこと。

以下のバージョンで確認した。

まずはプロジェクトの作成。

$ rails new myapp --skip-turbolinks
$ cd myapp
$ bundle exec rails g scaffold post title:string
$ bundle exec rails db:migrate

これで$ bundle exec rails sするとローカルサーバーが立ち上がり、http://localhost:3000/postsにアクセスできる。
以降、基本的にはこのページで動作確認していく。

プロダクションモード

Railsにはproductiondevelopmenttestの、3つの環境が用意されている。
ローカルで開発する際はデフォルトではdevelopmentが使われ、そして本番環境ではproductionを使うのが一般的。

だがdevelopmentproductionではアセットパイプラインの挙動が異なるため、その挙動を正しく学ぶためにはproductionもローカルで使う必要がある。

その具体的な方法は後述するが、事前に用意しておくべきものがいくつかある。

まずconfig/master.key。このファイルがないと、production環境でサーバーを起動することが出来ない。
rails newしたときに生成されるが、Gitの管理から外すのが一般的なので(デフォルトでもそうなっている)、作業環境によっては存在しないかもしれないので注意が必要。

そしてデーターベースの設定。
先程scaffoldで作成したモデルはdevelopment環境のデータベースにしか存在しないので、production環境でもそのデータベースを見るようにしておく。
以下のようにconfig/database.ymlを編集する。

--- a/config/database.yml
+++ b/config/database.yml
@@ -22,4 +22,4 @@ test:
 
 production:
   <<: *default
-  database: db/production.sqlite3
+  database: db/development.sqlite3

概要

アセットパイプラインでは、以下の処理を順番に行っていく。それぞれの内容については後述する。

  1. コンパイル
  2. 統合
  3. 圧縮
  4. ダイジェスト付与
  5. 完成したファイルをpublic/assets/に保存する

developmentでは235は行われない。14のみが自動的に行われる。
productionでは予め明示的に1~4を行って、その結果をpublic/assets/に保存しておく(この作業を、プリコンパイルという)。そしてそれをHTML側が参照する形になっている。

画像

画像では45のみを行う。

まず、使用したい画像をapp/assets/imagesのなかに置く。
例えばapp/assets/images/welcome.png

それを、image_tagで呼び出す。

--- a/app/views/posts/index.html.erb
+++ b/app/views/posts/index.html.erb
@@ -2,6 +2,8 @@
 
 <h1>Posts</h1>
 
+<%= image_tag 'welcome.png' %>
+
 <table>
   <thead>
     <tr>

image_tagを使う時はapp/assets/imagesがルートパスになる。

これで$ bundle exec rails sを実行してhttp://localhost:3000/postsを見ると、無事画像が表示されている。

これはdevelopmentなので、productionでも確認してみる。
production環境でローカルサーバーを立ち上げるには、以下のコマンドを実行する。

$ bundle exec rails s -e production

こうするとサーバーが立ち上がるが、エラー画面になってしまっている。
表示されている場合はdevelopmentで表示したときのキャッシュが効いているだけなので、キャッシュをクリアすれば表示されないはず。

これを解決するため、一度サーバーを停止させて以下を行う。

$ bundle exec rails assets:precompile RAILS_ENV=production

すると、yarn.lockと、public/assets以下にいくつかのファイルが生成される。
これがプリコンパイルである。

生成されたファイルのなかにはpublic/assets/welcome-xxx.pngもある。
xxxの部分には乱数のような文字列があると思うが、これがダイジェストである。ブラウザのキャッシュ対策のために付与される。

だがこの状態で再度productionでサーバーを立ち上げてページを確認しても、やはり表示されない。

ブラウザの開発者ツールのコンソールログで確認すると、画像だけでなくJavaScriptファイルやスタイルシートも見つからないというエラーが出ている。
該当するファイルは先程のプリコンパイルpublic/assets/に生成されているのだが、それを読み込めていない。

実はproductionでアセットを読み込むにはもう一つ、やっておくべき設定がある。
それが、RAILS_SERVE_STATIC_FILESという環境変数である。
この値が存在しないと、アセットを正しく読み込んでくれない。
値さえ存在すれば中身はなんでもよいのだが、今回は1にしておく。

$ export RAILS_SERVE_STATIC_FILES=1

これで再度サーバーを起動すれば、画像が表示され、コンソールログに出ていたエラーも消えているはず。

なお、RAILS_SERVE_STATIC_FILESという環境変数を見ているのはデフォルトではproductionだけなので、developmentの動作には影響しない。

マニフェストファイル

JavaScriptファイルとスタイルシートについては、ダイジェスト付与だけでなく、コンパイル、統合、圧縮も行う。

developmentではコンパイルとダイジェスト付与だけなので、まずはそれを見ていく。

アセットパイプラインにおいては、個々のJavaScriptファイルやスタイルシートを読み込むのではなく、マニフェストファイルと呼ばれるものを読み込むのが原則になっている。
もちろん個々に読み込むことも可能であり、それについては後述する。

マニフェストファイルは設定ファイルのようなもので、利用するJavaScriptファイルやスタイルシートをここで指定しておくことで、マニフェストファイルを読み込んだときにまとめて読み込まれる仕組みになっている。

実はマニフェストファイルはデフォルトで既に作成されており、しかも既にapp/views/layouts/application.html.erbで読み込まれている。

<%= stylesheet_link_tag    'application', media: 'all' %>
<%= javascript_include_tag 'application' %>

JavaScriptファイルはjavascript_include_tagで、スタイルシートstylesheet_link_tagで読み込まれる。
前者はapp/assets/javascripts/、後者はapp/assets/stylesheets/がルートパスになるので、上記の場合は以下の2つのファイルをマニフェストファイルとして読み込んでいることになる。

  • app/assets/javascripts/application.js
  • app/assets/stylesheets/application.css

2つのファイルにはいろいろと書かれているが、特に重要と思われるのは、どちらにも登場するrequire_tree .という記述。
require_treeは、指定されたディレクトリ以下の全てのファイルを読み込む。
今回はカレントディレクトリを指しているので、それ以下の全てのファイルを読み込む。

例として、app/assets/javascripts/sample.jsapp/assets/stylesheets/sample.cssを作成して確認してみる。

diff --git a/app/assets/javascripts/sample.js b/app/assets/javascripts/sample.js
new file mode 100644
index 0000000..8d46f25
--- /dev/null
+++ b/app/assets/javascripts/sample.js
@@ -0,0 +1 @@
+console.log('This is sample.js');
diff --git a/app/assets/stylesheets/sample.css b/app/assets/stylesheets/sample.css
new file mode 100644
index 0000000..ccf22a0
--- /dev/null
+++ b/app/assets/stylesheets/sample.css
@@ -0,0 +1,3 @@
+h1 {
+  color: blue;
+}

この状態でdevelopmentでサーバーを起動してページを見てみると、h1が青くなっており、コンソールログにはThis is sample.jsと表示されているはず。

また、ページの<head>の中身を見てみると、以下のようになっており、マニフェストファイルで指定したファイルが全てダイジェスト付きになって読み込まれている。

    <link rel="stylesheet" media="all" href="/assets/posts.self-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.css?body=1" />
<link rel="stylesheet" media="all" href="/assets/sample.self-d9b74f2b3926f9fbae3ce3ea3396c2c9ef70eafed818d184eab580a54595ce02.css?body=1" />
<link rel="stylesheet" media="all" href="/assets/scaffolds.self-8d6bd2e12bf8382f31c1c80fa0db365cd6866611c10f592870958f179690ed59.css?body=1" />
<link rel="stylesheet" media="all" href="/assets/application.self-f0d704deea029cf000697e2c0181ec173a1b474645466ed843eb5ee7bb215794.css?body=1" />
    <script src="/assets/rails-ujs.self-3b600681e552d8090230990c0a2e8537aff48159bea540d275a620d272ba33a0.js?body=1"></script>
<script src="/assets/activestorage.self-0525629bb5bac7ed5f2bfc58a9679d75705e426dafd6957ae9879db97c8e9cbe.js?body=1"></script>
<script src="/assets/action_cable.self-69fddfcddf4fdef9828648f9330d6ce108b93b82b0b8d3affffc59a114853451.js?body=1"></script>
<script src="/assets/cable.self-8484513823f404ed0c0f039f75243bfdede7af7919dda65f2e66391252443ce9.js?body=1"></script>
<script src="/assets/posts.self-877aef30ae1b040ab8a3aba4e3e309a11d7f2612f44dde450b5c157aa5f95c05.js?body=1"></script>
<script src="/assets/sample.self-ebf98f6f89562fbfb67cf4e34fbb1605c9aa1492e9c9d8cdc11495da3a1ad7fa.js?body=1"></script>
<script src="/assets/application.self-eba3cb53a585a0960ade5a8cb94253892706bb20e3f12097a13463b1f12a4528.js?body=1"></script>

マニフェストファイルの具体的な書き方については以下を参照。
railsguides.jp

コンパイル

ダイジェスト付与以外にdevelopmentproductionで共通して行われる処理として、コンパイルがある。
これは、CoffeeScriptなどを、ブラウザが解釈できる形に変換してくれる機能のことである。

app/assets/javascripts/cs.coffeeを作成して確認してみる。

diff --git a/app/assets/javascripts/cs.coffee b/app/assets/javascripts/cs.coffee
new file mode 100644
index 0000000..3f32e7a
--- /dev/null
+++ b/app/assets/javascripts/cs.coffee
@@ -0,0 +1,2 @@
+name = "Coffee Script"
+console.log "My name is #{name}!"

コンソールログを見るとMy name is Coffee Script!と表示されている。

ページが読み込んでいるsample.self-xxx.jsの中身を見ると、以下のようにJavaScriptの記法になっている。

console.log('This is sample.js');

しかし、ES2015+のトランスパイルは行われないので、注意が必要。
以下のようなコードは、トランスパイルされることなくそのまま出力される。

diff --git a/app/assets/javascripts/sample.js b/app/assets/javascripts/sample.js
index 1980b45..2171aa1 100644
--- a/app/assets/javascripts/sample.js
+++ b/app/assets/javascripts/sample.js
@@ -1 +1,2 @@
-console.log('This is sample.js');
+const hoge = BigInt;
+console.log(hoge);

そのため、BigIntをサポートしている最新版のChromeでは動くが、サポートしていないSafari12.0.1では以下のエラーになる。

ReferenceError: Can't find variable: BigInt

JavaScriptスタイルシートをプリコンパイルする

続いて、production環境での挙動を見てみる。

そのためにまず、プリコンパイルを行う。

$ rails assets:precompile RAILS_ENV=production

すると、エラーが出るはず。

rails aborted!
Uglifier::Error: Unexpected token: keyword (const). To use ES6 syntax, harmony mode must be enabled with Uglifier.new(:harmony => true).

これは、JavaScriptの圧縮を行っているライブラリがES2015+に対応していないのが原因。
config/environments/production.rbを編集して設定を変える必要がある。

diff --git a/config/environments/production.rb b/config/environments/production.rb
index a52e199..ab7d911 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -23,7 +23,7 @@ Rails.application.configure do
   config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
 
   # Compress JavaScripts and CSS.
-  config.assets.js_compressor = :uglifier
+  config.assets.js_compressor = Uglifier.new(harmony: true)
   # config.assets.css_compressor = :sass
 
   # Do not fallback to assets pipeline if a precompiled asset is missed.

その上で再度プリコンパイルすると、上手くいくはず。

productionで起動してページを見てみると、sample.jscs.coffeesample.cssの内容が正しく反映されている。
しかし<head>を見ると、読み込まれているのはapplication-xxxだけになっている。
これは、マニフェストファイルで指定したファイルをひとつにまとめているためである。これが、「概要」で述べた「統合」である。

    <link rel="stylesheet" media="all" href="/assets/application-79a17794ee143f211af72e257c0af173f9903be5aab393f962afb547892b4671.css" />
    <script src="/assets/application-4a7737dfce7f40904dec8d86d4ffb0662fab4f59c9bc0a9f975e6eee1a4381f4.js"></script>

先程ES2015+の対応を行ったが、それはあくまで圧縮できるようになったというだけで、コードの変換が行われているわけではない。
そのためSafari12.0.1でページを見るとやはりエラーになる。
BigIntはあまり使わないと思うが、constletなどもそのまま出力されるため、IE対応が必要な場合などは注意が必要。

マニフェストファイルを変える

マニフェストファイルはapp/assets/javascripts/application.jsapp/assets/stylesheets/application.cssである必要はない。
例えば、application.jsの階層を変えてみる。

renamed:    app/assets/javascripts/application.js -> app/assets/javascripts/base/application.js

erb側でパスを変えれば、何の問題もなく読み込める。

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 3a1b560..ebd1eb6 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -6,7 +6,7 @@
     <%= csp_meta_tag %>
 
     <%= stylesheet_link_tag    'application', media: 'all' %>
-    <%= javascript_include_tag 'application' %>
+    <%= javascript_include_tag 'base/application' %>
   </head>

ただ、require_tree .マニフェストファイルのカレントディレクトリ以下を読み込むので、その対象外であるapp/assets/javascripts/cs.coffeeapp/assets/javascripts/sample.jsは読み込まれない。

この挙動は、developmentでもproductionでも同じ。

個別に読み込む

特定のページだけで読み込みたいファイルなどは、そのファイルを直接指定することも出来る。

例として、http://localhost:3000/posts/newでのみ、app/assets/javascripts/sample.jsを読み込ませてみる。

javascript_include_tagで、読み込めばいい。

diff --git a/app/views/posts/new.html.erb b/app/views/posts/new.html.erb
index fb1e2a1..d692c12 100644
--- a/app/views/posts/new.html.erb
+++ b/app/views/posts/new.html.erb
@@ -3,3 +3,5 @@
 <%= render 'form', post: @post %>
 
 <%= link_to 'Back', posts_path %>
+
+<%= javascript_include_tag 'sample.js' =%>

まずはdevelopmentで確認。
http://localhost:3000/posts/newを開くと、エラーページが表示されてしまう。

これを解消するには、config/initializers/assets.rbRails.application.config.assets.precompile += %w( *.js )を追記しなければならない。

diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 4b828e8..d6464dc 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -12,3 +12,4 @@ Rails.application.config.assets.paths << Rails.root.join('node_modules')
 # application.js, application.css, and all non-JS/CSS in the app/assets
 # folder are already added.
 # Rails.application.config.assets.precompile += %w( admin.js admin.css )
+Rails.application.config.assets.precompile += %w( *.js )

これでサーバーを立ち上げ直すと、上手くいく。
app/assets/javascripts/sample.jshttp://localhost:3000/posts/newでのみ読み込まれ、他のページでは読み込まれない。

プリコンパイルすれば、productionでも問題なく動く。