1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Automatically set Link header for each stylesheet and script

<link rel="preload"> elements[0] can be serialized in `Link`
headers[1] to allow the browser to preload them before
it parsed the HTML body.

It is particularly useful for scripts included at the bottom
of the document.

[0] https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content
[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
This commit is contained in:
Jean Boussier 2020-07-28 11:57:36 +02:00
parent 70c9f39039
commit a9012af688
3 changed files with 50 additions and 22 deletions

View file

@ -86,11 +86,11 @@ module ActionView
def javascript_include_tag(*sources) def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
early_hints_links = [] preload_links = []
sources_tags = sources.uniq.map { |source| sources_tags = sources.uniq.map { |source|
href = path_to_javascript(source, path_options) href = path_to_javascript(source, path_options)
early_hints_links << "<#{href}>; rel=preload; as=script" preload_links << "<#{href}>; rel=preload; as=script"
tag_options = { tag_options = {
"src" => href "src" => href
}.merge!(options) }.merge!(options)
@ -100,7 +100,7 @@ module ActionView
content_tag("script", "", tag_options) content_tag("script", "", tag_options)
}.join("\n").html_safe }.join("\n").html_safe
request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request send_preload_links_header(preload_links)
sources_tags sources_tags
end end
@ -136,11 +136,11 @@ module ActionView
def stylesheet_link_tag(*sources) def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
early_hints_links = [] preload_links = []
sources_tags = sources.uniq.map { |source| sources_tags = sources.uniq.map { |source|
href = path_to_stylesheet(source, path_options) href = path_to_stylesheet(source, path_options)
early_hints_links << "<#{href}>; rel=preload; as=style" preload_links << "<#{href}>; rel=preload; as=style"
tag_options = { tag_options = {
"rel" => "stylesheet", "rel" => "stylesheet",
"media" => "screen", "media" => "screen",
@ -149,7 +149,7 @@ module ActionView
tag(:link, tag_options) tag(:link, tag_options)
}.join("\n").html_safe }.join("\n").html_safe
request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request send_preload_links_header(preload_links)
sources_tags sources_tags
end end
@ -281,12 +281,12 @@ module ActionView
crossorigin: crossorigin crossorigin: crossorigin
}.merge!(options.symbolize_keys)) }.merge!(options.symbolize_keys))
early_hints_link = "<#{href}>; rel=preload; as=#{as_type}" preload_link = "<#{href}>; rel=preload; as=#{as_type}"
early_hints_link += "; type=#{mime_type}" if mime_type preload_link += "; type=#{mime_type}" if mime_type
early_hints_link += "; crossorigin=#{crossorigin}" if crossorigin preload_link += "; crossorigin=#{crossorigin}" if crossorigin
early_hints_link += "; nopush" if nopush preload_link += "; nopush" if nopush
request.send_early_hints("Link" => early_hints_link) if respond_to?(:request) && request send_preload_links_header([preload_link])
link_tag link_tag
end end
@ -482,6 +482,16 @@ module ActionView
type type
end end
end end
def send_preload_links_header(preload_links)
if respond_to?(:request) && request
request.send_early_hints("Link" => preload_links.join("\n"))
end
if respond_to?(:response) && response
response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
end
end
end end
end end
end end

View file

@ -84,7 +84,7 @@ class RoutedRackApp
end end
class BasicController class BasicController
attr_accessor :request attr_accessor :request, :response
def config def config
@config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config| @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config|

View file

@ -9,23 +9,33 @@ ActionView::Template::Types.delegate_to Mime
class AssetTagHelperTest < ActionView::TestCase class AssetTagHelperTest < ActionView::TestCase
tests ActionView::Helpers::AssetTagHelper tests ActionView::Helpers::AssetTagHelper
attr_reader :request attr_reader :request, :response
class FakeRequest
attr_accessor :script_name
def protocol() "http://" end
def ssl?() false end
def host_with_port() "localhost" end
def base_url() "http://www.example.com" end
def send_early_hints(links) end
end
class FakeResponse
def headers
@headers ||= {}
end
end
def setup def setup
super super
@controller = BasicController.new @controller = BasicController.new
@request = Class.new do @request = FakeRequest.new
attr_accessor :script_name
def protocol() "http://" end
def ssl?() false end
def host_with_port() "localhost" end
def base_url() "http://www.example.com" end
def send_early_hints(links) end
end.new
@controller.request = @request @controller.request = @request
@response = FakeResponse.new
@controller.response = @response
end end
def url_for(*args) def url_for(*args)
@ -499,6 +509,14 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js") assert_dom_equal %(<script src="/javascripts/foo.js"></script>), javascript_include_tag("foo.js")
end end
def test_should_set_preload_links
stylesheet_link_tag("http://example.com/style.css")
javascript_include_tag("http://example.com/all.js")
expected = "<http://example.com/style.css>; rel=preload; as=style,<http://example.com/all.js>; rel=preload; as=script"
assert_equal expected, @response.headers["Link"]
end
def test_image_path def test_image_path
ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
end end