TL; DR(最初に結論)
2024年にrails newするなら、dartsass-railsを使おう。
そして、sass/scssファイルに画像のURLを指定する場合はimage-url
ではなくurl
を使おう。
/* app/assets/stylesheets/foo.scss */ .my-image { /* image-urlではなくurlを使う */ background-image: url('bg.png'); }
はじめに
Qiitaに書こうと思ったけど、記事としてきれいにまとまらないのでこっちに雑にまとめます。
たとえばRailsアプリで背景画像を出したいとき、かつSassを使っているとき、画像のURLはimage-url
で指定していました。
/* app/assets/stylesheets/foo.scss */ .my-image { background-image: image-url('bg.png'); }
ちなみにbg.pngはapp/assets/images/bg.png
に配置されている前提です。
上のscssファイルはプリコンパイルされるとダイジェスト付きのURLに変わります。(image-url
もurl
に変わる)
.my-image { background-image: url(/assets/bg-31822...4a19b.png); }
が、image-url
はsass-railsやsassc-railsが提供している関数です。
そして、sass-railsやsassc-railsはすでに開発が止まっていて、現在開発が継続しているのはdartsass-railsです。
ただし、dartsass-railsではimage-url
は使えません。
冒頭のscssファイルをプリコンパイルするとurlの中身はダイジェスト付きのパスになるものの、image-url
がimage-url
のままになり、結果として無効なCSSになります。
.my-image { /* dartsass-railsだとimage-urlがそのまま残る */ background-image: image-url(/assets/bg-31822...4a19b.png); }
じゃあどうしたらいいのかというと、scssにurl
を指定します。
/* app/assets/stylesheets/foo.scss */ .my-image { background-image: url('bg.png'); }
こうすると正しいCSSが出力されます。
.my-image { background-image: url(/assets/bg-31822...4a19b.png); }
なお、この記事ではimage-url
を対象にしていますが、asset-url
, font-url
, video-url
, audio-url
も考え方は同じです。
コラム:Railsで使えるSassのgemは3種類
歴史的経緯により、Railsで使えるSassのgemは、sass-rails、sassc-rails、dartsass-railsの3種類です。
さらにややこしいことに、sass-rails 6はsassc-railsのラッパーです。
This gem is now only just a wrapper around sassc-rails.
よって、これら3つのgemを古いものから順に並べると、以下のようになります。
- sass-rails 5以下(Rubyで実装されたオリジナルのSass。開発終了)
- sass-rails 6 = sassc-rails(Cで実装されたSass。開発終了)
- dartsass-rails(Dartで実装されたSass)
このほかにも、gemを使わずにnpmとして提供されているSassを使う、というアプローチもあります。 いろいろあってややこしいですね。
僕の疑問
上記のような挙動を知って、僕は以下のような疑問を持ちました。
image-url
ってそもそもどこで定義されてるの??url(...)
って標準のCSSなのに、なんでdartsass-railsだとプリコンパイルされたらダイジェスト付きのパスに変わるの??
それぞれ調査した結果を以下にまとめます。
セルフアンサー:image-url
はどこで定義されてるの??
image-url
はsprockets gemのimage_url
メソッドにマッピングされます。
# https://github.com/rails/sprockets/blob/v3.7.3/lib/sprockets/sass_processor.rb def image_url(path) asset_url(path, type: :image) end
asset_url
メソッドはsassc-railsに定義されていて、これがさらにasset_path
メソッドを呼び出します。
こうした一連の流れにより、image-url
の引数として指定したパスはasset_path
メソッドでダイジェスト付きのパスに変換されます。
# https://github.com/sass/sassc-rails/blob/v2.1.2/lib/sassc/rails/template.rb def asset_path(path, options = {}) path = path.value path, _, query, fragment = URI.split(path)[5..8] path = sprockets_context.asset_path(path, options) query = "?#{query}" if query fragment = "##{fragment}" if fragment ::SassC::Script::Value::String.new("#{path}#{query}#{fragment}", :string) end def asset_url(path, options = {}) ::SassC::Script::Value::String.new("url(#{asset_path(path, options).value})") end
しかし、dartsass-railsではasset_url
メソッドやasset_path
メソッドに相当する処理がありません。
セルフアンサー:なんでurl
がダイジェスト付きのパスに変わるの??
これはsprockets-rails 3.3でurl
に指定したパスをダイジェスト付きのパスに変換する機能が導入されたためです。
つまり、これはdartsass-railsではなく、sprockets-railsの機能です。
この機能は正規表現で実現されています。
# https://github.com/rails/sprockets-rails/blob/v3.5.1/lib/sprockets/rails/asset_url_processor.rb module Sprockets module Rails # Resolve assets referenced in CSS `url()` calls and replace them with the digested paths class AssetUrlProcessor REGEX = /url\(\s*["']?(?!(?:\#|data|http))(?<relativeToCurrentDir>\.\/)?(?<path>[^"'\s)]+)\s*["']?\)/ def self.call(input) context = input[:environment].context_class.new(input) data = input[:data].gsub(REGEX) do |_match| path = Regexp.last_match[:path] "url(#{context.asset_path(path)})" end context.metadata.merge(data: data) end end end end
実装上はurl(...)
みたいな文字列を置換するだけなので、image-url(...)
でもhoge-url(...)
でも、正規表現にマッチした文字列なら何でも()
内のパスがダイジェスト付きのパスに変換されます。
/* app/assets/stylesheets/foo.scss */ .my-image { /* hoge-urlはCSSとして無効 */ background-image: hoge-url('bg.png'); }
.my-image { /* だが、url(...)の形式ならimage-urlでもhoge-urlでも何でも変換される */ background-image: hoge-url(/assets/bg-31822...4a19b.png); }
また、sprockets-railsが提供している機能なので、SassではないプレーンなCSSでもアセットプリコンパイルの対象になっていればurl
がダイジェスト付きのパスに変換されます。
/* app/assets/stylesheets/foo.css */ .my-image { /* scssやsassではなく、プレーンなCSSとしてurlを指定する */ background-image: url(bg.png); }
.my-image { /* アセットプリコンパイルの対象になっていればダイジェスト付きのパスに変換される */ background-image: url(/assets/bg-31822...4a19b.png); }
ちなみにsprocets-rails 3.2以前ではurl
はプリコンパイルしても何も変わらず、そのまま出力されます*1。
/* sprockets-sass 3.2ではプリコンパイルしても何も変化なし */ .my-image { background-image: url(bg.png); }
まとめ
わかったことをまとめるとこんな感じです。
image-url
はsass-railsやsassc-railsが提供している関数image-url
はdartsass-railsには存在しない- バージョン3.3以降のsprockets-railsは
url
をダイジェスト付きのパスに変換してくれる - dartsass-railsは
url
を使うしかない - sass-railsやsassc-railsはsprockets-rails 3.3以上がインストールされていれば、
image-url
もurl
も両方使える
表にするとこんな感じでしょうか。
組み合わせ | |||
---|---|---|---|
sass-rails, sassc-rails | Y | Y | |
dartsass-rails | Y | ||
sprockets-rails 3.2以下 | Y | ||
sprockets-rails 3.3以上 | Y | Y | |
image-url | ✅ | ✅ | ❌ 2 |
url | ❌ 1 | ✅ | ✅ |
- ❌ 1 =
()
内のパスは変化しない(ダイジェスト付きにならない) - ❌ 2 = パスにダイジェストは付くものの、
image-url
がimage-url
のまま残るため、無効なCSSになる
2024年現在でrails newする場合、Sassを使いたいならdartsass-railsを使うことになるはずです(なぜならsass-railsやsassc-railsは開発が終了しているから)。
また、sprockets-rails 3.3がリリースされたのは2021年11月なので、3.2以前のsprockets-railsがインストールされることはまずないでしょう。
ということはSassで画像URLを指定する場合はimage-url
ではなくurl
を使う、というのが望ましい方法になりますね。
/* app/assets/stylesheets/foo.scss */ .my-image { background-image: url('bg.png'); }
参考文献
おまけ
Stack Overflowに同じような質問があり、まだ誰も回答していなかったので、僕が回答してみました(2013年の質問ですが・・・)。
あわせて読みたい
アセットプリコンパイル周りは初心者泣かせのややこしい挙動が満載です。画像がうまく表示できないときはこちらの記事も参考にしてみてください。
*1:これが原因でローカルでは画像が表示されるが、Herokuにデプロイすると画像が表示されない、というトラブルがよく起きていた