アセットパイプラインにハマる

前回の記事で書いたとおりRailsの環境を更新しました。
が、本番環境おけるアセットパイプラインでハマってしまいました。
ここで復習しておきます。

そもそもアセットパイプラインとは?

「app/assets」「lib/assets」「vendor/assets」など異なるパス上のjsやcssを同一のurl(例えばhttp://myapp:3000/assets/xxxx)で参照を可能にする仕組みです。(jsやcssの結合だったり圧縮だったり他にも色々仕事してます。)

アセットパイプラインの対象となるパスは「Rails.application.assets.paths」で参照できます。

#/myappが$RAILS_ROOT

$ script/rails console
irb(main):001:0> puts Rails.application.assets.paths
/myapp/app/assets/images
/myapp/app/assets/javascripts
/myapp/app/assets/stylesheets
/myapp//vendor/assets/javascripts
/myapp/vendor/assets/stylesheets
/myapp/vendor/bundle/ruby/2.1.0/gems/jquery-rails-3.1.0/vendor/assets/javascripts
/myapp/vendor/bundle/ruby/2.1.0/gems/coffee-rails-4.0.1/lib/assets/javascripts

アセットパイプライン(Asset Pipeline) - - Railsドキュメント
5分でわかる!? アセットパイプライン(Assets Pipeline) - Rails つまみぐい
RailsのAsset Pipelineの理解とgemで管理しないJavaScriptライブラリの配置 | EasyRamble
#279 Understanding the Asset Pipeline - RailsCasts

production環境におけるアセットパイプライン

このアセットパイプライン、production環境ではコンパイル済みのファイルを探す」という動きをとります。
つまりデプロイ前にコンパイルしないと、アセットパイプライン上に配置したファイルが404エラーとなってしまいます。

解決策

1. 手動でプリコンパイルする

rakeタスクでアセットファイルをプリコンパイルしておくことができます。

$ bundle exec rake assets:precompile RAILS_ENV=production

そうすると$RAILS_ROOT/public/assetsの中にプリコンパイルされたファイルができあがります。
が、この方法は当然ながらプリコンパイルしたファイルをリポジトリに登録する必要がありちょっとイマイチかも。
そこで、

2.動的にコンパイルする

サーバがリクエストを受け取った際、コンパイル済みのファイルが見つからない場合にその場でコンパイルを行う設定が可能です。
設定は$RAILS_ROOT/config/environments/production.rbで行います。

#config/environments/production.rb

# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = true     #trueに設定すると動的コンパイルが「有効」

これならリポジトリへの登録は不要です。しかし動的コンパイルなので初回リクエスト時にはレスポンスが犠牲になります。
それが嫌なら、

3.デプロイ時にプリコンパイルをする

capistrano-railsを利用することでcapistranoによるデプロイ時、デプロイ先にプリコンパイルしたアセットファイルを作成することができます。
Gemfileに定義して

#Gemfile

…
gem 'capistrano-rails'

インストールして

$ bundle install

Capfileで読み込めばOK。

#Capfilerequire 'capistrano/rails/assets'

これでデプロイ先の$RAILS_ROOT/current/public/assetsコンパイル済みのファイルが配置されます。

されるはずだったが・・・

上記のCapistranoによるプリコンパイルを実施したのですが、上手くいきませんでした。

こんなエラーが出たのです。

Started GET "/assets/first.png" for 106.188.44.126 at 2014-06-27 00:51:18 +0900

ActionController::RoutingError (No route matches [GET] "/assets/first.png"):

上記のfirst.png$RAILS_ROOT/app/assets/imagesに格納しています。
アセットパイプライン上に置いているし、プリコンパイルもしたのにファイルが見つからない?

このエラーの原因は直接パスを指定していることにありました。
上記の画像はviewの中で直接パスを指定していました。

<img serc="assets/first.png" />

developmentならば特に問題ありませんが、production環境ではこれをpublic/assetsに置く必要があります。
そこでプリコンパイルを実施するわけですが、Rails4からはプリコンパイルの成果物がRails3の頃と変更になってます。
上記first.pngを例に取ると、Rails3の頃は

public/assets/first.png
public/assets/first-xxxxx.png  #xxxxxはハッシュ値

と元のファイル名そのままのものとハッシュ値が付いたファイルが生成されていたのに対し、Rails4では

public/assets/first-xxxxx.png  #xxxxxはハッシュ値

ハッシュ値付きのものしか生成されません。

そのため直接パスを指定していると書いてある通りのファイル名がリクエストされ、404となります。
これに対する解決方法はasset_pathメソッドを利用することです。

<img src="<%= asset_path 'first.png' %>" />

asset_pathにアセットパイプライン上のファイル名を指定するだけで、development、productionそれぞれに適した形に変換してくれます。

#development
<img src="/assets/first.png" />
#production
<img src="/assets/first-696d2d60937a6e31851de94c4e646e3b.png" />

Rails4ではbackground:url("assets/hoge.png")の書き方は動かない話
asset_path - リファレンス - - Railsドキュメント
Railsのasset_pathは何をやってくれてるのか - (゚∀゚)o彡 sasata299's blog


これで解決!と思っていたら別の問題が起きていました。

Started GET "/index.css" for 127.0.0.1 at 2014-06-28 18:11:40 +0900

ActionController::RoutingError (No route matches [GET] "/index.css"):

index.cssはトップページでのみ使いたいため、application.cssではrequireせずvendor/assets/stylesheetsに置いています。
しかリクエストされたファイル名を見ると、ハッシュ値なしのファイル名が・・・
app/public/assets内を確認すると、このcssはプリコンパイルされていませんでした。

原因はRails4でのassets:precompileにおけるコンパイル対象にありました。
Rails4ではアセットファイルのプリコンパイル対象が以下のように定義されています。

@assets.precompile = [ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
                     /(?:\/|\\|\A)application\.(css|js)$/ ]

読み解くと

  1. app/assets/というパスを含み
  2. .js、.css以外の資産 または
  3. application.js、application.css

がプリコンパイルの対象となります。

つまり、アセットパイプライン上のファイル≠プリコンパイル対象ってことなの!


じゃあどうすればいいのか。application.cssに含めるしかないのか?

コンパイル対象を増やしたい場合はconfig/application.rb内で設定することが可能です。

#config/application.rb

config.assets.precompile += %w(index.css)

Rails4でGemの資産がAssets Precomplieに含まれないときは · THINKING MEGANE

これで全て解決しました。
でもこの方法だとapplication.js|cssでrequireしないファイルが増えるたびに設定を追加しないといけなくて微妙だなー、とも思います。

うーん、Rails難しい!

Ruby on Rails 4 アプリケーションプログラミング

Ruby on Rails 4 アプリケーションプログラミング

実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング

実践Ruby on Rails 4 現場のプロから学ぶ本格Webプログラミング