From aad7fbde68684547959dcccc2102c978d5347a78 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sun, 18 Feb 2007 23:54:20 +0000 Subject: Added caching option to AssetTagHelper#stylesheet_link_tag and AssetTagHelper#javascript_include_tag [DHH] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@6164 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- actionpack/CHANGELOG | 13 ++ actionpack/lib/action_controller/base.rb | 2 +- .../lib/action_view/helpers/asset_tag_helper.rb | 171 ++++++++++++++++++--- actionpack/test/controller/render_test.rb | 6 + .../test/fixtures/public/javascripts/bank.js | 1 + .../test/fixtures/public/javascripts/robber.js | 1 + .../test/fixtures/public/stylesheets/bank.css | 1 + .../test/fixtures/public/stylesheets/robber.css | 1 + actionpack/test/template/asset_tag_helper_test.rb | 124 +++++++++++++-- 9 files changed, 291 insertions(+), 29 deletions(-) create mode 100644 actionpack/test/fixtures/public/javascripts/bank.js create mode 100644 actionpack/test/fixtures/public/javascripts/robber.js create mode 100644 actionpack/test/fixtures/public/stylesheets/bank.css create mode 100644 actionpack/test/fixtures/public/stylesheets/robber.css (limited to 'actionpack') diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 2110d223f9..8380af610b 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,18 @@ *SVN* +* Added caching option to AssetTagHelper#stylesheet_link_tag and AssetTagHelper#javascript_include_tag [DHH]. Examples: + + stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is false => + + + + + stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is true => + + + ...when caching is on, all.css is the concatenation of style1.css, styleB.css, and styleX2.css. + Same deal for JavaScripts. + * Work around the two connection per host browser limit: use asset%d.myapp.com to distribute asset requests among asset[0123].myapp.com. Use a DNS wildcard or CNAMEs to map these hosts to your asset server. See http://www.die.net/musings/page_load_time/ for background. [Jeremy Kemper] * Added default mime type for CSS (Mime::CSS) [DHH] diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ecbe15bada..c012abc448 100755 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -879,7 +879,7 @@ module ActionController #:nodoc: if text.is_a?(String) if response.headers['Status'][0..2] == '200' && !response.body.empty? - response.headers['Etag'] ||= %("#{Digest::MD5.hexdigest(text)}") + response.headers['Etag'] = %("#{Digest::MD5.hexdigest(text)}") if request.headers['HTTP_IF_NONE_MATCH'] == response.headers['Etag'] response.headers['Status'] = "304 Not Modified" diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 850615632f..e72e23b20b 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -28,6 +28,10 @@ module ActionView # for server load balancing. See http://www.die.net/musings/page_load_time/ # for background. module AssetTagHelper + ASSETS_DIR = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : "public" + JAVASCRIPTS_DIR = "#{ASSETS_DIR}/javascripts" + STYLESHEETS_DIR = "#{ASSETS_DIR}/stylesheets" + # Returns a link tag that browsers and news readers can use to auto-detect # an RSS or ATOM feed. The +type+ can either be :rss (default) or # :atom. Control the link options in url_for format using the @@ -93,22 +97,70 @@ module ActionView # # ... # *see below + # + # * = The application.js file is only referenced if it exists + # + # You can also include all javascripts in the javascripts directory using :all as the source: + # + # javascript_include_tag :all # => + # + # + # ... + # + # + # + # + # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available for + # all subsequently included files. They + # + # == Caching multiple javascripts into one + # + # You can also cache multiple javascripts into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is false => + # + # + # ... + # + # + # + # + # javascript_include_tag :all, :cache => true # when ActionController::Base.perform_caching is true => + # + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false => + # + # + # + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when ActionController::Base.perform_caching is false => + # def javascript_include_tag(*sources) options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + cache = options.delete("cache") - if sources.include?(:defaults) - sources = sources[0..(sources.index(:defaults))] + - @@javascript_default_sources.dup + - sources[(sources.index(:defaults) + 1)..sources.length] + if ActionController::Base.perform_caching && cache + joined_javascript_name = (cache == true ? "all" : cache) + ".js" + joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name) - sources.delete(:defaults) - sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") - end + if !File.exists?(joined_javascript_path) + File.open(joined_javascript_path, "w+") do |cache| + javascript_paths = expand_javascript_sources(sources).collect { |source| javascript_path(source) } + cache.write(join_asset_file_contents(javascript_paths)) + end + end - sources.collect do |source| - source = javascript_path(source) - content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) - end.join("\n") + content_tag("script", "", { + "type" => "text/javascript", "src" => javascript_path(joined_javascript_name) + }.merge(options)) + else + expand_javascript_sources(sources).collect do |source| + content_tag("script", "", { "type" => "text/javascript", "src" => javascript_path(source) }.merge(options)) + end.join("\n") + end end # Register one or more additional JavaScript files to be included when @@ -148,12 +200,64 @@ module ActionView # stylesheet_link_tag "random.styles", "/css/stylish" # => # # + # + # You can also include all styles in the stylesheet directory using :all as the source: + # + # stylesheet_link_tag :all # => + # + # + # + # + # == Caching multiple stylesheets into one + # + # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is false => + # + # + # + # + # stylesheet_link_tag :all, :cache => true # when ActionController::Base.perform_caching is true => + # + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is false => + # + # + # + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when ActionController::Base.perform_caching is true => + # def stylesheet_link_tag(*sources) options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } - sources.collect do |source| - source = stylesheet_path(source) - tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) - end.join("\n") + cache = options.delete("cache") + + if ActionController::Base.perform_caching && cache + joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" + joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name) + + if !File.exists?(joined_stylesheet_path) + File.open(joined_stylesheet_path, "w+") do |cache| + stylesheet_paths = expand_stylesheet_sources(sources).collect { |source| stylesheet_path(source) } + cache.write(join_asset_file_contents(stylesheet_paths)) + end + end + + tag("link", { + "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", + "href" => stylesheet_path(joined_stylesheet_name) + }.merge(options)) + else + options.delete("cache") + + expand_stylesheet_sources(sources).collect do |source| + tag("link", { + "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => stylesheet_path(source) + }.merge(options)) + end.join("\n") + end end # Computes the path to an image asset in the public images directory. @@ -216,6 +320,7 @@ module ActionView # request protocol. def compute_public_path(source, dir, ext) source += ".#{ext}" if File.extname(source).blank? + if source =~ %r{^[-a-z]+://} source else @@ -245,15 +350,45 @@ module ActionView # modification time as its cache-busting asset id. def rails_asset_id(source) ENV["RAILS_ASSET_ID"] || - File.mtime("#{RAILS_ROOT}/public/#{source}").to_i.to_s rescue "" + File.mtime(File.join(ASSETS_DIR, source)).to_i.to_s rescue "" end # Break out the asset path rewrite so you wish to put the asset id # someplace other than the query string. def rewrite_asset_path!(source) asset_id = rails_asset_id(source) - source << "?#{asset_id}" if defined?(RAILS_ROOT) && !asset_id.blank? + source << "?#{asset_id}" if !asset_id.blank? + end + + def expand_javascript_sources(sources) + case + when sources.include?(:all) + all_javascript_files = Dir[File.join(JAVASCRIPTS_DIR, '*.js')].collect { |file| File.basename(file).split(".", 0).first } + sources = ((@@javascript_default_sources.dup & all_javascript_files) + all_javascript_files).uniq + + when sources.include?(:defaults) + sources = sources[0..(sources.index(:defaults))] + + @@javascript_default_sources.dup + + sources[(sources.index(:defaults) + 1)..sources.length] + + sources.delete(:defaults) + sources << "application" if File.exists?(File.join(JAVASCRIPTS_DIR, "application.js")) + end + + sources + end + + def expand_stylesheet_sources(sources) + if sources.first == :all + sources = Dir[File.join(STYLESHEETS_DIR, '*.css')].collect { |file| File.basename(file).split(".", 1).first } + else + sources + end + end + + def join_asset_file_contents(paths) + paths.collect { |path| File.read(File.join(ASSETS_DIR, path.split("?").first)) }.join("\n\n") end end end -end +end \ No newline at end of file diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index fb784d3e02..21f5fb1f51 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -335,6 +335,12 @@ class RenderTest < Test::Unit::TestCase assert_equal expected_etag, @response.headers['Etag'] end + def test_etag_should_govern_renders_with_layouts_too + get :builder_layout_test + assert_equal etag_for("\n\n

Hello

\n

This is grand!

\n\n
\n"), @response.headers['Etag'] + end + + protected def assert_deprecated_render(&block) assert_deprecated(/render/, &block) diff --git a/actionpack/test/fixtures/public/javascripts/bank.js b/actionpack/test/fixtures/public/javascripts/bank.js new file mode 100644 index 0000000000..4a1bee7182 --- /dev/null +++ b/actionpack/test/fixtures/public/javascripts/bank.js @@ -0,0 +1 @@ +// bank js \ No newline at end of file diff --git a/actionpack/test/fixtures/public/javascripts/robber.js b/actionpack/test/fixtures/public/javascripts/robber.js new file mode 100644 index 0000000000..eb82fcbdf4 --- /dev/null +++ b/actionpack/test/fixtures/public/javascripts/robber.js @@ -0,0 +1 @@ +// robber js \ No newline at end of file diff --git a/actionpack/test/fixtures/public/stylesheets/bank.css b/actionpack/test/fixtures/public/stylesheets/bank.css new file mode 100644 index 0000000000..ea161b12b2 --- /dev/null +++ b/actionpack/test/fixtures/public/stylesheets/bank.css @@ -0,0 +1 @@ +/* bank.css */ \ No newline at end of file diff --git a/actionpack/test/fixtures/public/stylesheets/robber.css b/actionpack/test/fixtures/public/stylesheets/robber.css new file mode 100644 index 0000000000..0fdd00a6a5 --- /dev/null +++ b/actionpack/test/fixtures/public/stylesheets/robber.css @@ -0,0 +1 @@ +/* robber.css */ \ No newline at end of file diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 33705b8e7c..8b8a426bf6 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -6,7 +6,25 @@ class AssetTagHelperTest < Test::Unit::TestCase include ActionView::Helpers::AssetTagHelper def setup - Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") + silence_warnings do + ActionView::Helpers::AssetTagHelper.send( + :const_set, + :JAVASCRIPTS_DIR, + File.dirname(__FILE__) + "/../fixtures/public/javascripts" + ) + + ActionView::Helpers::AssetTagHelper.send( + :const_set, + :STYLESHEETS_DIR, + File.dirname(__FILE__) + "/../fixtures/public/stylesheets" + ) + + ActionView::Helpers::AssetTagHelper.send( + :const_set, + :ASSETS_DIR, + File.dirname(__FILE__) + "/../fixtures/public" + ) + end @controller = Class.new do attr_accessor :request @@ -23,7 +41,7 @@ class AssetTagHelperTest < Test::Unit::TestCase end def teardown - Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT) + ActionController::Base.perform_caching = false ENV["RAILS_ASSET_ID"] = nil end @@ -53,9 +71,10 @@ class AssetTagHelperTest < Test::Unit::TestCase %(javascript_include_tag("xmlhr.js")) => %(), %(javascript_include_tag("xmlhr", :lang => "vbscript")) => %(), %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n), - %(javascript_include_tag(:defaults)) => %(\n\n\n), - %(javascript_include_tag(:defaults, "test")) => %(\n\n\n\n), - %(javascript_include_tag("test", :defaults)) => %(\n\n\n\n) + %(javascript_include_tag(:defaults)) => %(\n\n\n\n), + %(javascript_include_tag(:all)) => %(\n\n), + %(javascript_include_tag(:defaults, "test")) => %(\n\n\n\n\n), + %(javascript_include_tag("test", :defaults)) => %(\n\n\n\n\n) } StylePathToTag = { @@ -71,6 +90,8 @@ class AssetTagHelperTest < Test::Unit::TestCase %(stylesheet_link_tag("/dir/file")) => %(), %(stylesheet_link_tag("dir/file")) => %(), %(stylesheet_link_tag("style", :media => "all")) => %(), + %(stylesheet_link_tag(:all)) => %(\n), + %(stylesheet_link_tag(:all, :media => "all")) => %(\n), %(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n), %(stylesheet_link_tag("http://www.example.com/styles/style")) => %() } @@ -106,20 +127,19 @@ class AssetTagHelperTest < Test::Unit::TestCase end def test_javascript_include_tag - Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT) + ENV["RAILS_ASSET_ID"] = "" JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } - Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") ENV["RAILS_ASSET_ID"] = "1" assert_dom_equal(%(\n\n\n\n), javascript_include_tag(:defaults)) end def test_register_javascript_include_default - Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT) + ENV["RAILS_ASSET_ID"] = "" ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' - assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults) + assert_dom_equal %(\n\n\n\n\n), javascript_include_tag(:defaults) ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' - assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults) + assert_dom_equal %(\n\n\n\n\n\n\n), javascript_include_tag(:defaults) end def test_stylesheet_path @@ -127,6 +147,7 @@ class AssetTagHelperTest < Test::Unit::TestCase end def test_stylesheet_link_tag + ENV["RAILS_ASSET_ID"] = "" StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end @@ -169,6 +190,89 @@ class AssetTagHelperTest < Test::Unit::TestCase image_tag(source) assert_equal copy, source end + + + def test_caching_javascript_include_tag_when_caching_on + ENV["RAILS_ASSET_ID"] = "" + ActionController::Base.perform_caching = true + + assert_dom_equal( + %(), + javascript_include_tag(:all, :cache => true) + ) + + assert File.exists?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(), + javascript_include_tag(:all, :cache => "money") + ) + + assert File.exists?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + ensure + File.delete(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + File.delete(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + + def test_caching_javascript_include_tag_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + ActionController::Base.perform_caching = false + + assert_dom_equal( + %(\n\n), + javascript_include_tag(:all, :cache => true) + ) + + assert !File.exists?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'all.js')) + + assert_dom_equal( + %(\n\n), + javascript_include_tag(:all, :cache => "money") + ) + + assert !File.exists?(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'money.js')) + end + + def test_caching_stylesheet_link_tag_when_caching_on + ENV["RAILS_ASSET_ID"] = "" + ActionController::Base.perform_caching = true + + assert_dom_equal( + %(), + stylesheet_link_tag(:all, :cache => true) + ) + + assert File.exists?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(), + stylesheet_link_tag(:all, :cache => "money") + ) + + assert File.exists?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + ensure + File.delete(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + File.delete(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end + + def test_caching_stylesheet_include_tag_when_caching_off + ENV["RAILS_ASSET_ID"] = "" + ActionController::Base.perform_caching = false + + assert_dom_equal( + %(\n), + stylesheet_link_tag(:all, :cache => true) + ) + + assert !File.exists?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'all.css')) + + assert_dom_equal( + %(\n), + stylesheet_link_tag(:all, :cache => "money") + ) + + assert !File.exists?(File.join(ActionView::Helpers::AssetTagHelper::STYLESHEETS_DIR, 'money.css')) + end end class AssetTagHelperNonVhostTest < Test::Unit::TestCase -- cgit v1.2.3