give IT a try

プログラミング、リモートワーク、田舎暮らし、音楽、etc.

Rails + Sassでimage-urlを使うのは古い。画像の指定はurlだけでOK

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-urlurlに変わる)

.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-urlimage-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.

https://github.com/rails/sass-rails/releases/tag/v6.0.0

よって、これら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に指定したパスをダイジェスト付きのパスに変換する機能が導入されたためです。

github.com

つまり、これは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-urlurlも両方使える

表にするとこんな感じでしょうか。

組み合わせ
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-urlimage-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');
}

参考文献

github.com

おまけ

Stack Overflowに同じような質問があり、まだ誰も回答していなかったので、僕が回答してみました(2013年の質問ですが・・・)。

stackoverflow.com

あわせて読みたい

アセットプリコンパイル周りは初心者泣かせのややこしい挙動が満載です。画像がうまく表示できないときはこちらの記事も参考にしてみてください。

qiita.com

*1:これが原因でローカルでは画像が表示されるが、Herokuにデプロイすると画像が表示されない、というトラブルがよく起きていた