WEBサイトではほとんどのページで共通する部品ってのがありますよね。
ヘッダとかフッタとか。
普通、Railsではそういうパーツはレイアウトに書いてしまうんだけど、それだと当然全てのページのレスポンスに共通パーツが含まれてしまうわけです。
これってレスポンスのサイズを無駄にデカくしてますよね。
そもそも、毎回同じ文字列がレスポンスに含まれるのはDRYじゃなくてダサい気がします。
そう考えだすと居ても立ってもいられず、共通パーツをJSに埋め込んで、動的にレンダリングしてやろうなんて思ったわけです。
まず、やったのはレイアウトに書いてたヘッダパーツを切り出す作業です。
元々のapp/views/layouts/application.html.erbが
<body>
<header>
(中略)
</header>
<%= yield %>
</body>
みたいな感じで、ヘッダを含んでいたのを
<body>
<div id="page-header"></div>
<%= yield %>
</body>
こんな風にヘッダのプレースホルダとなるDIVタグだけにします。
そして、ヘッダのコンテンツは
app/assets/templates/header.html.erb
<header>
(中略)
</header>
に切り出します。
次に、切り出したヘッダ用コンテンツをjs内に文字列として埋め込みます。
そして、ページロード完了時にヘッダコンテンツをjQueryでHTML内に描画してやるようにします。
app/assets/javascripts/application.js.erb
var header_html = '<%= escape_to_js_str render_asset('header.html') %>';
$(function(){
$('#page-header').append($(header_html));
});
このファイルはrubyスクリプトを含めるためにerbで書いてます。
render_assetメソッドはapp/assetsディレクトリ内のファイルを文字列として読み込む為のもので、escape_to_js_strメソッドはその文字列をJS内で文字列リテラルとして問題ない様にエスケープするためのものです。
これらのメソッドは次のapp/helpers/asset_helper.rb内に定義しました。
module AssetHelper
# アセットファイルをパースして返す
def render_asset(path)
depend_on_asset(path)
body = assets.find_asset(path).body
end
# 受け取った文字列をJS文字列リテラルとして問題ないようにエスケープ
def escape_to_js_str(str)
escape_map = { '\\' => '\\\\', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'" }
escape_map.each do |search, replace|
str.gsub!(search, replace)
end
str
end
end
depend_on_asset(path)がミソで、これをやっておかないとheader.html.erbを変更した時に、application.js.erbの再コンパイルが走ってくれず、ヘッダの変更が反映されないという問題が起きます。いやぁ、この方法を見つけるまで、結構ソースを読み込みましたよ。
次に、このままではapplication.js.erb内でAssetHelperが使えないので、以下のコードをconfig/initializers/assets.rb内に追加します。
Sprockets::Context.send :include, AssetHelper
これで、一応は目的通り動作してくれるようになりました。
ですが、まだ問題が有ります。
rake assets:precompile
を実行するとheader.html.erbまでコンパイルされてpublic/assets/ディレクトリ内にheader-1234567890abcdef.htmlみたいなファイルが生成されてしまうんです。これは不要なファイルなので生成されないようにします。
config/initializers/assets.rbに以下のコードを追加しました。
# .js .css .htmlファイルはコンパイルしないようにする
precompile_target = lambda do |filename, path|
puts filename
path =~ /app\/assets/ && !%w(.js .css .html).include?(File.extname(filename))
end
Rails.application.config.assets.precompile = [
precompile_target,
/(?:\/|\\|\A)application\.(css|js)$/
]
これは何をしているかというと、js、css、htmlの拡張子を持つファイルはプリコンパイルしないようにして、ただしapplication.cssとapplication.jsだけはプリコンパイルするようにしています。
この辺りの具体的な説明はRailsのアセットのプリコンパイル対象の追加と除外方法に書いてます。
これで完成です!
ヘッダ以外の共通パーツも同様にJSに切り出してやれば、各ページのレスポンスにはそのページに最低限必要なHTMLだけを含めることが出来、トラフィックの削減が期待できて、すごく幸せになれそうですね!
と思って頑張ってみたんですが、実際にやってみると落とし穴がありました。
切り出したコンテンツ内では動的なデータを含めることが難しいので、使える範囲は非常に限定的でした。例えば、ヘッダー部分に表示するコンテンツがDBから取得したデータに依存している場合なんかは完璧にアウトです。
なので、頑張った割にはあんまり効果がないという、私的には残念な結果になってしまいました。
でも、全ページに共通な静的なコンテンツがたくさんあるサイトとかだと使う意味があるかもしれません。