Rails4のproduction環境でのみ、画像が表示されない問題
問題の概要
先日、弊社サービス STORYS.JP をRails3からRails4.0.2にアップデートしました。
Rails4はRails3に比べて、development環境でのページロードにかかる時間が1/10ぐらいになりました。
読み込みに 1500ms ほどかかっていたのが、Rails4にするだけで本番並みの 150ms で返ってくるようになり、本当に非常に快適に開発ができるようになりました。
しかし、便利なものには弊害も多く、様々な問題がおきます。
その中でも、解決に苦しんだ問題が、 これまでは正常に表示されていた画像が、Rails4のproduction環境でのみ表示されない問題 です。
問題の原因
問題の原因は、 app/assets/images
以下に置いてある画像を直接パスを指定して読み込んでいることでした。
STORYS.JP では基本的に、HTML, SCSSで画像 app/assets/images/hoge.png
を参照したいときは次のように書いていました。
<img src="/assets/hoge.png" />
<div style="background-image:url(/assets/hoge.png)" />
SCSSではこんな感じです。
.hoge {
background-image: url(/assets/hoge.png);
}
しかしこの /assets/hoge.png
という書き方では、Rails4からは表示されなくなりました。
Rails4での assets:precompile
productionにdeployするときに実行する assets:precompile
ですが、
Rails4からは digest無しのjs, css, image は作成しない仕様になりました
digestとはjs, css, 画像等を assets:precompile
するとファイル名の後ろにつく文字列のことです。
/assets/application-d34a3eba396a045b8c71d1605256e1a2.css
つまり、今までは app/assets/images/hoge.png
を assets:precompile
したときに得られる成果物は、
public/assets/hoge.png
public/assets/hoge-○○○○.png
であったのに対し
public/assets/hoge-○○○○.png
となるようになりました。
したがってproduction環境では /assets/hoge.png
と指定したファイルを参照することはできません。
解決法1 asset_path関数, image-url関数を使う
一番順当な解決法です。画像のパスの指定をそのまま書くのではなく、Railsが提供している関数を呼ぶことで、productionのときにはdigestを勝手に付与してくれます。
<img src="<%= asset_path "hoge.png" %>" />
<div style="background-image:url(<%= asset_path "hoge.png" %>)" />
SCSSではこんな感じです。
.hoge {
background-image: image-url("hoge.png");
}
Rails GuideのAsset Pipelineの項にもう少し詳しく載っています。
解決法2 public/assets 以下に画像を直接置く
HTML,SCSSをわざわざRailsの関数を使って書きなおすのはめんどくさい・・・そう考えていた時にあることを思いつきました。
そもそもdigestを付与するのは、あるファイルを更新したときに、ブラウザ側で持っているキャッシュとの相違が無いようにするためのものです。
それはjavascriptやstylesheetならば話はよくわかります。application.jsは良く更新されるので。
しかし、画像ファイルで名前を変更せずに中身を変更することはほとんどありません。そうするならば名前も変更するか、新しいファイルを作るからです。
ならばそもそも app/assets/images
以下はdigestを付与しないオプションは無いのかと思い、調べてみたところ、 assets:precompile
を管理している sprockets-rails に次のような記述がありました。
Only compiles digest filenames. Static non-digest assets should simply live in public/.
つまり、コンパイルする必要のないものは、そのままpublic以下に置けと言っています。
確かに、 assets:precompile
のそもそもを考えればその通りだと思い、今まで app/assets/images
以下にあったものを public/assets
以下に移動しました。
cp -r app/assets/images public/assets/
これならば、既存のHTMLやSCSSを書き換える必要すらなく完璧である・・・と思ったのですが、1つ特定の環境において問題が起こりました。
それは capistranoでのdeploy です。
ここについては詳しくは調べていませんが、gitの管理下に public/assets
を含めていても、deploy時には public/assets
以下は削除されてしまうようです。
したがって、git管理下に public/assets/hoge.png
が存在したとしても、deployされた先には public/assets/hoge.png
は存在しないので画像は表示されません。
もしかしたらcapistranoは public/assets 以下がgit管理下になることを想定されていないのかもしれません。
解決法3 public/images 以下に画像を置く
そこで STORYS.JP ではこの解決法3を採用しました。
画像は基本的に public/images
以下に置くようにしました。
<img src="/images/icons/hoge.png" />
<div style="background-image:url(/images/icons/hoge.png)" />
.hoge {
background-image: url(/images/icons/hoge.png);
}
これの欠点は、既存のファイルを /assets/icons/hoge.png
-> /images/icons/hoge.png
に書きなおす必要があること、またproduction環境の nginx, apache 等に記述されているであろう /assets/ へのキャッシュのポリシーを /images にも設定しなくてはいけないことです。
しかし、こうすることでproduction環境でも、無事画像が表示でき、capistranoでのdeployも問題ありませんでした。
この解決法3の良い所はもう一つあります。
それは、完全に app/assets/images
を使わないことで、間違って /assets/hoge.png
の記述をしにくいところです。
解決法1では、development環境で /assets/hoge.png
のような記述をしても問題なく表示されます。
しかし、production環境では動きません
これは結構やっかいで、コードレビュー等をしていたとしても、軽微な違いなだけに気づきにくく、deployしてから表示されないことに気が付きます。
<img src="/assets/icons/hoge.png" />
<img src="<%= asset_path "icons/hoge.png" %>"/>
<!-- この違いを見分けるのは結構難しい !>
しかし、常に app/assets/images
を使わないようにすることで、 <%= asset_path "icons/hoge.png" %>
のような記述はできなくなり、問題発見が容易になります。
Rails4のproduction環境でのみ画像が表示されない問題の解決策 は以上になります。
もしこの問題が起きた場合は、自分たちの状況、必要な物に応じて好きな選択肢を選んでください。