Edited at

コントローラ・アクションごとに固有のasset (CSS, JS) を読み込ませる

More than 5 years have passed since last update.

Railから外れたことをやろうとして苦労したのん。


環境


  • ruby 2.0.0p247

  • Rails 4.0.1

  • テンプレートエンジンには haml を使用


やりたいこと


  • コントローラ・アクションごとに固有のスタイルシート・スクリプトを用意したい。(全体で使うものだけをasset pipelineでまとめてほしい。)

  • たとえば、"users#index" なら、application.css(.scss) と application.js に加えて以下のファイルも読み込んで欲しい。


    • users.css(.scss)

    • users/index.css(.scss)

    • users.js

    • users/index.js




とりあえず思いつく方法 (うまくいかない)

application.css.scss, application.js から require_tree . の行を削除した後、application.html.haml にコントローラ名・コントローラ名 + アクション名のスタイルシート・スクリプトを読み込むように指定する。


app/views/layouts/application.html.haml

= stylesheet_link_tag controller_name, media: "all"

= stylesheet_link_tag "#{controller_name}/#{action_name}", media: "all"
= javascript_link_tag controller_name, media: "all"
= javascript_link_tag "#{controller_name}/#{action_name}", media: "all"


問題点


  • そのままでは、asset precompile の対象にならない。

  • app/assets/{stylesheets,javascripts} 以下に該当するファイルが無いと、/stylesheets または /javascripts 以下に飛ばされてしまう。


Asset precompileの対象にする

Asset pipelineを利用している場合 (Sprockets が有効になっている場合) は単に


config/environments/production.rb

config.assets.precompile += %w[.js .css]


とすると、production 環境でプリコンパイルした際 (RAILS_ENV=production rake assets:precompile)、asset path (Rails.configuration.assets.paths) イカにある全てのファイルがコンパイル対象となる。

通常は余計なファイル (pipelineで結合されるため、直接読み込まないファイル) が生成されるのみで大きな影響は無いが、Sass/SCSS などで依存関係があるファイル (別ファイルに変数を定義している場合など。具体的には bootstrap-sass のファイル) がコンパイルエラーとなってしまう。

app/assets 以下のファイルだけを対象とするには、lambda を渡して判定させれば良い。1番目の引数はコンパイル後の相対パス (起点は asset path)、2番目は処理されるファイル名の絶対パスとなる。


config/environments/production.rb

config.assets.precompile << lambda { |path, fn|

fn =~ %r{app/assets} and [".js", ".css"].include?(File.extname(path))
}


制限事項


  • app/assets 以下に置かれているファイルは、asset pipelineによって require されるファイル (直接HTMLから呼び出さないファイル) もプリコンパイル対象となる。

  • 通常は余計なファイルが生成される程度だが、変数の定義が別ファイルで行われているなど単体で成り立たない場合はエラーになる。その場合は、条件判定を工夫する必要がある。(元ファイルのフルパスが渡されるので、柔軟に条件判定は出来る。)


Assetファイルが存在する場合のみにタグを挿入する

適当な所で ActionView::Helpers::AssetUrlHelper にメソッドを追加する。

module ActionView

module Helpers
module AssetUrlHelper
def stylesheet_exists?(source)
Rails.application.assets.find_asset(
source + compute_asset_extname(source, :type => :stylesheet)
)
end

def stylesheet_link_tag_if_exists(*sources)
options = sources.extract_options!.stringify_keys
sources.select! { |s| stylesheet_exists? s }
sources.push(options)
stylesheet_link_tag(*sources)
end

def javascript_exists?(source)
Rails.application.assets.find_asset(
source + compute_asset_extname(source, :type => :javascript)
)
end

def javascript_include_tag_if_exists(*sources)
options = sources.extract_options!.stringify_keys
sources.select! { |s| javascript_exists? s }
sources.push(options)
javascript_include_tag(*sources)
end
end
end
end

stylesheet_exists? または javascript_exists? ファイルの存在を判定してタグを挿入する。冗長だが、application.html.haml に1回書くだけなので気にしなくて良いかも。


app/views/layouts/application.html.haml

- if stylesheet_exists? controller_name

= stylesheet_link_tag controller_name, media: "all"
- if stylesheet_exists? "#{controller_name}/#{action_name}"
= stylesheet_link_tag "#{controller_name}/#{action_name}", media: "all"
- if javascript_exists? controller_name
= javascript_link_tag controller_name
- if javascript_exists? "#{controller_name}/#{action_name}"
= javascript_link_tag "#{controller_name}/#{action_name}"

stylesheet_link_tag_if_exists または javascript_include_tag_if_exists を使うと簡潔に書けるが、変なところで空白が入るのでHTMLが汚くなる。


app/views/layouts/application.html.haml

= stylesheet_link_tag_if_exists controller_name, media: "all"

= stylesheet_link_tag_if_exists "#{controller_name}/#{action_name}", media: "all"
= javascript_link_tag_if_exists controller_name
= javascript_link_tag_if_exists "#{controller_name}/#{action_name}"


まとめ


  • Railから外れるとめんどい

  • 全部のファイルをpipelineでまとめた方が効率は良いはず

  • にゃんぱすー