この記事は
基本はRuby on Rails Guides: Asset Pipeline の訳ですが簡略化や自分の解釈で意訳した部分が多々あります。気になる点あったらコメントください。
Assets Pipelineとは
Asset(アセット)とは、訳すと「資産」のこと。Assets Pipelineは画像やJavaScript、CSSを高速でリクエストを捌けるようにしたRails 3.1より標準搭載された仕組みです。
Assets Pipelineで出来ること
Fingerprinting
コンテンツベースのファイル名に更新することによるキャッシュ支援
Precompileを利用した高レベル言語でのコーディング
CSSに対してSass/SCSS/LESS、JavaScriptに対してCoffeeScript等の中間言語が使用可能
ERBも使用可能
Assetの連結
複数のJavascriptやCSSをまとめてリクエスト数を減らす
Assetの最小化と圧縮
CSSは空白削除、JavaScriptはif-elseを三項演算子化など、サイズ縮小のために最適化
Assets Pipelineはデフォルトで有効です。無効にしたければconfig/application.rbにて
1
config.assets.enabled = false
を設定してください。また、アプリケーション作成時であれば
1
rails new appname --skip-sprockets
と–skip-sporocketsオプションを付ける事でも無効にできます。ただし、何らかの理由が無ければ、デフォルトの状態で有効にしておくと高速化の面でも有利ですのでそのままにしておくべきでしょう。
Assets Pipelineの使い方
app/assetsディレクトリの中にAssetを配置します。今まではpublicに全て配置していて、利用は可能ですが、全て静的ファイルとしてのみ扱われます。
production環境のデフォルトでは事前にプリコンパイルを行い、public/assetsに配置して動作時にapp/assetsから直接出力せずパフォーマンスに配慮したデプロイを行います。
Assetの配置場所は
app/assets
lib/assets
vendor/assets
の三箇所があります。appはアプリケーション固有のもの、libは複数のアプリケーションでも利用できるもの、vendorはサードベンダーのJSプラグインやCSSフレームワークといったように使い分けると良いでしょう。
Asset Pathの検索
マニフェストやヘルパーよりファイルを参照すると、デフォルトではimages、javascripts、stylesheetsのサブディレクトリ内からファイルを探します。例えば、
1
2
3
app/assets/javascripts/home.js
lib/assets/javascripts/moovinator.js
vendor/assets/javascripts/slider.js
をJSマニフェストから参照するとすると
1
2
3
//= require home
//= require moovinator
//= require slider
ヘルパからだと
1
2
3
<%= javascript_include_tag "home" %>
<%= javascript_include_tag "moovinator" %>
<%= javascript_include_tag "slider" %>
にて参照できます。
indexファイルはフォルダ名指定でlib/assets/library_name/index.jsを
1
//= require library_name
と参照可能です。ちなみにRails.application.config.assets.pathsを見るとAsset検索Pathが分かります。また、以下のようにconfig/application.rbにパスを追加できます
1
config.assets.paths << Rails.root.join("app", "assets", "flash")
重要なことは、検索したいファイルはプリコンパイル対象となっていることです。そうしないとproduction環境では利用できなくなってしまいます。
Fingerprintingとは
Fingerprintingとはファイル内容も加味されたファイルネーミングの仕組みです。ファイルの中身が変更されるとファイル名も更新されます。
これにより、CDNであったり、複数の別のサーバー上に配置したとしても、ファイル名によってコンテンツ内容が一意である事が分かり、キャッシュ最適化を行う事が可能になります。
Assets Pipelineではファイル名にMD5ハッシュが挿入されます。global.cssが元ファイルとすると、global-908e25f4bf641868d8683022a5b62f54.cssのようなファイル名となります。
Assets Pipeline導入前のRailsは時間ベースの文字列クエリを付加していましたが、以下のデメリットがありました:
クエリだと確実にキャッシュする事を保証出来ない
CDN等でリクエストがキャッシュされないケースがあった
複数のサーバに静的コンテンツを配置するとファイル名が異なる
ファイル時間が同一ファイルでも異なる事があるために無駄に読み込みがかかるケースがあった
同一のファイルでも再度デプロイしたらキャッシュが利用されないことがある
更新日時が変更されたら再度読み込みを強いることになる
これらのデメリットをファイル内容に基づくファイル名の付加によって解消されました。
Fingerprintingはproduction環境で有効、その他の環境は無効にデフォルトはなっています。替えたい場合はconfig.assets.digest設定を変更して下さい。
Assetへのリンク
Assets Pipelineでは通常のリンクヘルパを利用すればOKです
1
2
3
<%= stylesheet_link_tag "application" %>
<%= javascript_include_tag "application" %>
<%= image_tag "rails.png" %>
accetsパスへの検索、production環境であればMD5をファイル名に付加更新もここで行われます
1
rails-af27b6a414e6da00003503148be9b409.png
CSS/JavaScript/ERB
Assets PipelineはERBにて評価可能です。application.css.erbのようにerbの拡張子を付加すれば次のように利用できます。
1
.class { background-image: url(<%= asset_path 'image.png' %>) }
Base64形式として埋め込みヘルパもあります
1
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
application.js.cooffee.erbのようにCoffeeScriptにも利用できます
1
$('#logo').attr src: "<%= asset_path('logo.png') %>"
アセットの依存関係と連結~マニフェストファイルとディレクティブ
どのアセットを利用するかを決定するのにマニフェストファイルを利用します。CSS/JavaScriptにコメントにてディレクティブを記述します。また、Rails.application.config.assets.compressがtrueであれば連結します。連結することに単一ファイルになるのでリクエスト時間を減らせます。例えばapp/assets/javascripts/application.jsに以下のように記述します
1
2
3
4
// ...
//= require jquery
//= require jquery_ujs
//= require_tree .
//=で行を始めてディレクティブを記載します。jquery.jsとjquery_ujs.jsはjquery-rails gemが提供しています。
require_treeは再帰検索を行います。対象パスのサブディレクトリも検索してすべてのファイルを出力します。サブディレクトリを検索しないで特定のディレクトリを指定したい場合はrequire_directoryを利用して下さい。
requireは上から順に処理されますが、require_treeは何処に記載しても順序は関係無いです。連結した場合でも、特定のJavascriptが一番最初に来るように保証したいのであれば、requireも一番最初に記述してください
CSSの例は以下のようになります
1
2
3
4
/* ...
*= require_self
*= require_tree .
*/
requre_selfはこれが呼び出された順序で自分のCSSを読み込むようになります。require_treeを使うとJSと同様に現在のディレクトリのすべてのスタイルシートを読み込みます。
複数のSassファイルを利用する時はSassの@importルールを使用スべきです。ディレクティブで記載してしまうと全Sassファイルはそれぞれのスコープ下に存在することになって変数やmixinが定義されたドキュメント内でしか利用できなくなります。
Preprocessing
Assetの拡張子で前処理が決まります。projects.js.coffeeとすればCoffeeScriptが、projects.css.scssとすればSCSSが処理されます。
複数の拡張子を付加すれば前処理の多重化も可能です。projects.css.scss.erbとすれば、拡張子は右側からERB、SCSSと処理され、最終的にCSSとして応答します。
この拡張子の処理順序は重要で、projects.js.erb.coffeeと記載してしまうと、ERB記述のままCoffeeScriptインタプリタが処理されてしまいエラーとなってしまいます。projects.js.coffee.erbの順で記載してください。
JavaScriptの圧縮
JavaScriptは:closure :uglifier :yuiの3種類が選べます。closure-compiler・uglifier・yui-compressorの各gemが必要です。Gemfileにはデフォルトでuglifierがあります。ホワイトスペースを削除し、if-else文を三項演算子に変更するなどの最適化をします。
1
config.assets.js_compressor = :uglifier
JS圧縮を有効にするには以下の設定が必要です
1
config.assets.compress = true
Development環境
development環境では連結されずに別々のファイルとなります
1
2
3
//= require core
//= require projects
//= require tickets
の記載では
1
2
3
<script src="/assets/core.js?body=1" type="text/javascript"></script>
<script src="/assets/projects.js?body=1" type="text/javascript"></script>
<script src="/assets/tickets.js?body=1" type="text/javascript"></script>
となります。但し、
1
config.assets.debug = false
と、デバッグモードをオフにすれば連結状態になります。
サーバに初回リクエストにてアセットはコンパイル・キャッシュされ、304を返すようになります。マニフェスト記載のどれかが変更があると新しくコンパイルしてファイルを応答します。
デバッグモードは以下でも有効に出来ます。
1
2
<%= stylesheet_link_tag "application", :debug => true %>
<%= javascript_include_tag "application", :debug => true %>
パフォーマンス的にはデバッグモードは不利なので切り替えが提供されています
Production環境
production環境ではFingerprintを利用します。プリコンパイル時にファイル内容に応じてMD5値が生成されてファイル名が更新されてディスクに格納されます。挿入された名前はマニフェスト、ヘルパーで補完されます。
1
2
<%= javascript_include_tag "application" %>
<%= stylesheet_link_tag "application" %>
にて
1
2
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js" type="text/javascript"></script>
<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" type="text/css" />
となります。
config.assets.digest設定にてFingerprintのON/OFFを切り替えられます。production環境で有効、その他は無効になっています。通常はこのデフォルト設定は変更すべきではありません。キャッシュ制御がクライアント側で困難になるからです。
Assetsのプリコンパイル
development環境のデフォルトでは動的にAssetファイルがコンパイルされますが、production環境のデフォルトでは高速化を図るため、事前にコンパイルして静的ファイルとして配置するようになっています。これをプリコンパイルといいます。Railsではrakeタスクとして配布されています。
コンパイルされたAssetはconfig.assets.prefixに設定されたディレクトリに格納されます。デフォルトはpublic/assetsディレクトリです。
デプロイサーバー上でのプリコンパイルも可能ですが、ファイル書き込み権限が無い等、rakeタスクを直接実行出来ない時はローカルでrakeタスクを実行してください。実行するrakeタスクは、以下のようになります。
1
bundle exec rake assets:precompile
プリコンパイルの実行速度を早めるためにconfig.assets.initialize_on_precompile設定をfalseに設定してRailsアプリケーションのロードをプリコンパイル時には部分的に行うようにすることが可能となります。Herokuではこの設定は必須となっています。
config.assets.initialize_on_precompile設定をfalseにした際は、ローカルでプリコンパイルが正しく実行出来るか確認してからでプロイして下さい。アプリケーションのオブジェクトやメソッドを参照していたりするとエラーが発生する場合があるので注意する必要があります。
Capistano(v2.8.0以降)にはプリコンパイル実行を制御するレシピが用意されています。以下の行をCapfileに追加してください。
これによって、config.assets.prefixはshared/assetsディレクトリに設定されます。既にこのフォルダを利用していた場合は独自にrakeタスクを作成して下さい。
また、重要な点として、古いプリコンパイルされたAssetsファイルがページキャッシュ有効な間は正しく参照可能となるようにこのsharedフォルダはデプロイサーバー間で共有されている必要があります。
Assetsファイルをローカルでプリコンパイルする時は、デプロイ環境ではAsset関連のgemが必要なくなるので、以下のコマンドでassetsグループのgemを除外すると良いでしょう。
1
bundle install --without assets
プリコンパイルを行うファイルとして、application.js、application.cssとJS/CSSファイルではない全てのファイルが含まれます。もし、他にも独自のJS/CSSファイルを読み込みたい場合は以下のように追加設定する必要があります。
1
config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
この設定により、rakeタスクによるmanifest.ymlにはassetsファイルとそのFingerprintsの対応付けが行われ、ヘルパを呼び出すたびにSprocketsによるFingerprintへのマッピングリクエストを回避する事が出来ます。マニフェストファイルの例は以下のような感じです。
1
2
3
4
5
6
---
rails.png: rails-bd9ad5a560b5a3a7be0808c5cd76a798.png
jquery-ui.min.js: jquery-ui-7e33882a28fc84ad0e0e47e46cbf901c.min.js
jquery.min.js: jquery-8a50feed8d29566738ad005e19fe1c2d.min.js
application.js: application-3fdab497b8fb70d20cfc5495239dfc29.js
application.css: application-8af74128f904600e41a6e39241464e03.css
マニフェストファイルの配置場所はconfig.assets.prefixのルートディレクトリに配置されます。変更したい場合は以下にて設定出来ます。
1
config.assets.manifest = '/path/to/some/other/location'
Production環境でプリコンパイルされたファイルが見つからなければ、存在しないファイル名と共にSprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError の例外がRaiseされます。
サーバーの設定
プリコンパイルされたAssetファイルはファイルシステム上に静的ファイルとして配置され、Webサーバーより直接提供出来ます。Fingerprintingのメリットを十分に生かすため、以下のようにサーバー設定を行う事が推奨されます。
Apache:
1
2
3
4
5
6
7
<LocationMatch "^/assets/.*$">
Header unset ETag
FileETag None
# RFC says only cache for 1 year
ExpiresActive On
ExpiresDefault "access plus 1 year"
</LocationMatch>
nginx:
1
2
3
4
5
6
7
location ~ ^/assets/ {
expires 1y;
add_header Cache-Control public;
add_header ETag "";
break;
}
プリコンパイル時、gzip圧縮(.gz)ファイルも同時に作成されます。WebサーバーではしばしばGzip圧縮転送の設定を行って転送量の軽減を図りますが、動的な圧縮だと、CPUの使用も懸念して圧縮率に妥協をすることがあります。この問題を解決するために事前に最小サイズとなるようにgzip圧縮ファイルを高圧縮率で作成した物を直接ディスクより提供する事でサーバー負荷を低減させるように設定する事もできます。
Nginxではgzip_staticを有効にする事により可能になります。設定は以下の通り。
1
2
3
4
5
6
location ~ ^/(assets)/ {
root /path/to/public;
gzip_static on; # to serve pre-gzipped version
expires max;
add_header Cache-Control public;
}
このディレクティブは大凡のコンパイル済みWebサーバーで利用出来ますが、出来ない場合はモジュールを追加してコンパイルする必要がある場合があります
1
./configure --with-http_gzip_static_module
Phusion Passengerと一緒にnginxをコンパイルする場合はこのオプションを指定する必要があります。
Apacheでも可能ですがやや困難です。トライしたいならググってください。
動的コンパイル(Live Compilation)
動的コンパイルをやりたい環境がある場合はSprocketsに全てのリクエストを投げる事も出来ます。以下のように設定します
1
config.assets.compile = true
Assetの初回アクセス時にコンパイルされdevelopment環境よりも上位環境ではキャッシュされてMD5ハッシュ化されたファイル名が使用されます。
SprocketsはCache-Controlヘッダにmax-age=31536000を設定してきます。これはサーバー、クライアント間でリクエストされたデータは1年間キャッシュしても良いという取り決めになります。この事によりブラウザ等でのローカルキャッシュや中間キャッシュを行うことができるようになり、サーバーへのリクエスト削減が期待出来ます。
しかしながら、動的コンパイルは多くのメモリやサーバ負荷となり得るのでお勧め出来ません。
もし、JavaScriptランタイムが導入されていないproduction環境のサーバーデプロイしたい場合は以下をGemfileに追加してください
1
2
3
group :production do
gem 'therubyracer'
end
パイプラインのカスタマイズ
CSS圧縮
現在CSS圧縮エンジンとしてYUIがあります。YUIはCSSの最小化を行います。
1
config.assets.css_compressor = :yui
config.assets.compressはtrueにしておく必要があります。
JavaScript圧縮
JS圧縮は:closure、:uglifierand、:yuiの3種類から選択出来ます。それぞれ、closure-compiler、uglifier、yui-compressorのgemが必要になります。
デフォルトでGemfileにはuglifierが指定されています。これはNodeJSで書かれたUglifierJSのRubyラッパーです。スペース削減やif文を3項演算子に変換する等JSの論理変換を行ってサイズ最小化が行われます。以下のように指定してください
1
config.assets.js_compressor = :uglifier
JS圧縮指定時もconfig.assets.compressは有効である必要があります。
uglifierにはJSランタイムであるExecJSが必要になります。MacOSXやWindowsにはインストールされています。サポートされているOSに関してはExecJSのドキュメントをチェックして下さい。
独自圧縮ロジックの定義
自分で圧縮ロジックを定義する事もできます。compressメソッドで変換結果をリターンして下さい。
1
2
3
4
5
class Transformer
def compress(string)
do_something_returning_a_string(string)
end
end
利用するにはオブジェクトを指定します
1
config.assets.css_compressor = Transformer.new
assetsパスの変更
Asset公開パスのデフォルトは /assets です。3.1にアップデートする際に既にこのURIが利用されていた等の際には以下にてパスを変更する事も出来ます
1
config.assets.prefix = "/some_other_path"
X-Sendfileヘッダ
X-SendfileヘッダはWebサーバー自身がアプリケーションからのレスポンスをカスタマイズしてファイルをディスク上から直接読み込んで代わりにデータの受け渡しをします。デフォルトはオフですが、サポートされているなら有効にすることによりファイル提供の転送速度向上が期待できます。
Apache、nginxでサポートされており、以下をconfig/environments/production.rbに設定します
1
2
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
既に存在しているアプリケーションのアップグレード時等に、production環境以外のX-Sendfileに対応していない環境化にこの設定をコピペしないように注意してください。
キャッシュ
Sprocketsはdeveloment、production環境でRailsのデフォルトキャッシュストアを利用します。
デフォルトstore以外を指定出来る事が今後の課題です。
Gemsを利用したAssetsの追加
AssetsはGemsの形式でも外部ソースを取り込めます。
jquery-rails gem等が良い例です。一般的なJSライブラリを取り込めます。このgemにはRails::Engineが継承されたクラスを含んでいて、中に含まれているapp/assetsやlib/assets、vendor/assets等のディレクトをSprocketsの検索パスとして追加されます。
以上。あと、3.1以前からのアップデートの方法等載っていました。興味のある人は原文を参照してみてください。