template_streaming 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ == 0.0.1 2010-04-13
2
+
3
+ * Hi.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 George Ogata
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,163 @@
1
+ # Template Streaming
2
+
3
+ Rails plugin which enables progressive rendering for templates.
4
+
5
+ ## Background
6
+
7
+ A typical Rails client-side profile looks something like this:
8
+
9
+ <img
10
+ alt="Typical Rails Profile"
11
+ src="http://github.com/oggy/template_streaming/raw/master/doc/slow-profile.png"
12
+ style="width: 100%"
13
+ />
14
+
15
+ In almost all cases, this is highly suboptimal, as many resources, such as
16
+ external stylesheets, are static and could be loaded by the client while it's
17
+ waiting for the server response.
18
+
19
+ The trick is to output the response *progressively*--flushing the stylesheet
20
+ link tags out to the client before it has rendered the rest of the
21
+ page. Depending on how other external resources such javascripts and images are
22
+ used, they too may be flushed out early, significantly reducing the time for the
23
+ page to become interactive.
24
+
25
+ The problem is Rails has never been geared to allow this. Most Rails
26
+ applications use layouts, which require rendering the content of the page before
27
+ the layout. Since the global stylesheet tag is usually in the layout, we can't
28
+ simply flush the rendering buffer from a helper method.
29
+
30
+ Until now.
31
+
32
+ Template Streaming circumvents the template rendering order by introducing
33
+ *prelayouts*. A prelayout wraps a layout, and is rendered *before* the layout
34
+ and its content. By using the provided `flush` helper prior to yielding in the
35
+ prelayout, one can now output content early in the rendering process, giving
36
+ profiles that look more like:
37
+
38
+ <img
39
+ alt="Progressive Rendering Profile"
40
+ src="http://github.com/oggy/template_streaming/raw/master/doc/fast-profile.png"
41
+ style="width: 100%"
42
+ />
43
+
44
+ Also provided is a `#push(data)` method which can be used to send extra tags to
45
+ the client as their need becomes apparent. For instance, you may wish to `push`
46
+ out a stylesheet link tag only if a particular partial is reached which contains
47
+ a complex widget.
48
+
49
+ ## Example
50
+
51
+ Conventional wisdom says to put your external stylesheets in the HEAD of your
52
+ page, and your external javascripts at the bottom of the BODY (markup in
53
+ [HAML][haml]):
54
+
55
+ ### `app/views/prelayouts/application.html.haml`
56
+
57
+ !!! 5
58
+ %html
59
+ %head
60
+ = stylesheet_link_tag 'one'
61
+ = stylesheet_link_tag 'two'
62
+ - flush
63
+ = yield
64
+
65
+ ### `app/views/layouts/application.html.haml`
66
+
67
+ %body
68
+ = yield
69
+ = javascript_include_tag 'one'
70
+ = javascript_include_tag 'two'
71
+
72
+ With progressive rendering, however, this could be improved. As [Stoyan Stefanov
73
+ writes][stefanov], you can put your javascripts in the HEAD of your page if you
74
+ fetch them via AJAX and append them to the HEAD of your page dynamically. This
75
+ also reduces the time for the page to become interactive (e.g., scrollable),
76
+ giving an even greater perceived performance boost.
77
+
78
+ Of course, rather than using an external library for the AJAX call, we can save
79
+ ourselves a roundtrip by defining a `getScript` function ourselves in a small
80
+ piece of inline javascript. This is done by `define_get_script`
81
+ below. `get_script` then includes a call to this function which fetches the
82
+ script asynchronously, and then appends the script tag to the HEAD.
83
+
84
+ ### `app/views/prelayouts/application.html.haml`
85
+
86
+ !!! 5
87
+ %html
88
+ %head
89
+ = define_get_script
90
+ = stylesheet_link_tag 'one'
91
+ = stylesheet_link_tag 'two'
92
+ = get_script 'one'
93
+ = get_script 'two'
94
+ - flush
95
+ = yield
96
+
97
+ ### `app/views/layouts/application.html.haml`
98
+
99
+ %body
100
+ = yield
101
+
102
+ ### `app/helpers/application_helper.rb`
103
+
104
+ module ApplicationHelper
105
+ def define_get_script
106
+ javascript_tag do
107
+ File.read(Rails.public_path + '/javascripts/get_script.js')
108
+ end
109
+ end
110
+
111
+ def get_script(url)
112
+ javascript_tag do
113
+ "$.getScript('#{javascript_path(url)}');"
114
+ end
115
+ end
116
+ end
117
+
118
+ ### `public/javascripts/get_script.js`
119
+
120
+ //
121
+ // Written by Sam Cole. See http://gist.github.com/364746 for more info.
122
+ //
123
+ window.$ = {
124
+ getScript: function(script_src, callback) {
125
+ var done = false;
126
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
127
+ var script = document.createElement("script");
128
+ script.src = script_src;
129
+ script.onload = script.onreadystatechange = function() {
130
+ if ( !done && (!this.readyState ||
131
+ this.readyState === "loaded" || this.readyState === "complete") ) {
132
+ if(callback) callback();
133
+
134
+ // Handle memory leak in IE
135
+ script.onload = script.onreadystatechange = null;
136
+ if ( head && script.parentNode ) {
137
+ head.removeChild( script );
138
+ }
139
+
140
+ done = true;
141
+ }
142
+ };
143
+ head.insertBefore( script, head.firstChild );
144
+ }
145
+ };
146
+
147
+ The second profile was created using this code.
148
+
149
+ [haml]: http://haml-lang.com
150
+ [stefanov]: http://www.yuiblog.com/blog/2008/07/22/non-blocking-scripts
151
+ [get-script]: http://gist.github.com/364746
152
+
153
+ ## Note on Patches/Pull Requests
154
+
155
+ * Bug reports: http://github.com/oggy/template_streaming/issues
156
+ * Source: http://github.com/oggy/template_streaming
157
+ * Patches: Fork on Github, send pull request.
158
+ * Ensure patch includes tests.
159
+ * Leave the version alone, or bump it in a separate commit.
160
+
161
+ ## Copyright
162
+
163
+ Copyright (c) 2010 George Ogata. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ gem 'ritual'
2
+ require 'ritual'
3
+
4
+ spec_task :spec do |t|
5
+ t.libs << 'lib' << 'spec'
6
+ t.spec_files = FileList['spec/**/*_spec.rb']
7
+ end
8
+
9
+ spec_task :rcov do |t|
10
+ t.libs << 'lib' << 'spec'
11
+ t.pattern = 'spec/**/*_spec.rb'
12
+ t.rcov = true
13
+ end
14
+
15
+ rdoc_task do |t|
16
+ t.rdoc_dir = 'rdoc'
17
+ t.title = "Template Streaming #{version}"
18
+ t.rdoc_files.include('README*')
19
+ t.rdoc_files.include('lib/**/*.rb')
20
+ end
21
+
22
+ task :default => :spec
Binary file
Binary file
@@ -0,0 +1,172 @@
1
+ module TemplateStreaming
2
+ module Controller
3
+ def self.included(base)
4
+ base.alias_method_chain :render, :template_streaming
5
+ base.helper_method :flush, :push
6
+ end
7
+
8
+ def render_with_template_streaming(*args, &block)
9
+ # Only install our StreamingBody in the toplevel #render call.
10
+ @render_stack_height ||= 0
11
+ @render_stack_height += 1
12
+ begin
13
+ if @render_stack_height == 1
14
+ @performed_render = true
15
+ @streaming_body = StreamingBody.new(progressive_rendering_threshold) do
16
+ @performed_render = false
17
+ last_piece = render_without_template_streaming(*args, &block)
18
+ # The original render will clobber our response.body, so
19
+ # we must push the buffer ourselves.
20
+ push last_piece
21
+ end
22
+ response.body = @streaming_body
23
+ response.prepare!
24
+ else
25
+ render_without_template_streaming(*args, &block)
26
+ end
27
+ ensure
28
+ @render_stack_height -= 1
29
+ end
30
+ end
31
+
32
+ #
33
+ # Flush the current template's output buffer out to the client
34
+ # immediately.
35
+ #
36
+ def flush
37
+ unless @template.output_buffer.nil?
38
+ push @template.output_buffer.slice!(0..-1)
39
+ end
40
+ end
41
+
42
+ #
43
+ # Push the given data to the client immediately.
44
+ #
45
+ def push(data)
46
+ @streaming_body.push(data)
47
+ end
48
+
49
+ private # --------------------------------------------------------
50
+
51
+ #
52
+ # The number of bytes that must be received by the client before
53
+ # anything will be rendered.
54
+ #
55
+ def progressive_rendering_threshold
56
+ response.header['Content-type'] =~ %r'\Atext/html' or
57
+ return 0
58
+
59
+ case request.env['HTTP_USER_AGENT']
60
+ when /MSIE/
61
+ 255
62
+ when /Chrome/
63
+ # Note: Chrome's UA string includes "Safari", so it must precede.
64
+ 2048
65
+ when /Safari/
66
+ 1024
67
+ else
68
+ 0
69
+ end
70
+ end
71
+ end
72
+
73
+ # Only prepare once.
74
+ module Response
75
+ def self.included(base)
76
+ base.alias_method_chain :prepare!, :template_streaming
77
+ base.alias_method_chain :set_content_length!, :template_streaming
78
+ end
79
+
80
+ def prepare_with_template_streaming!
81
+ return if defined?(@prepared)
82
+ prepare_without_template_streaming!
83
+ @prepared = true
84
+ end
85
+
86
+ def set_content_length_with_template_streaming!
87
+ if body.is_a?(StreamingBody)
88
+ # pass
89
+ else
90
+ set_content_length_without_template_streaming!
91
+ end
92
+ end
93
+ end
94
+
95
+ module View
96
+ def self.included(base)
97
+ base.alias_method_chain :_render_with_layout, :template_streaming
98
+ end
99
+
100
+ def _render_with_layout_with_template_streaming(options, local_assigns, &block)
101
+ with_prelayout prelayout_for(options), local_assigns do
102
+ _render_with_layout_without_template_streaming(options, local_assigns, &block)
103
+ end
104
+ end
105
+
106
+ def with_prelayout(prelayout, locals, &block)
107
+ if prelayout
108
+ begin
109
+ @_proc_for_layout = lambda do
110
+ # nil out @_proc_for_layout else rendering with the layout will call it again.
111
+ @_proc_for_layout, original_proc_for_layout = nil, @_proc_for_layout
112
+ begin
113
+ block.call
114
+ ensure
115
+ @_proc_for_layout = original_proc_for_layout
116
+ end
117
+ end
118
+ render(:file => prelayout, :locals => locals)
119
+ ensure
120
+ @_proc_for_layout = nil
121
+ end
122
+ else
123
+ yield
124
+ end
125
+ end
126
+
127
+ def prelayout_for(options)
128
+ layout = options[:layout] or
129
+ return nil
130
+ # Views can call #render with :layout to render a layout
131
+ # *partial* which we don't want to interfere with. Only the
132
+ # interlal toplevel #render calls :layout with an
133
+ # ActionView::Template
134
+ layout.is_a?(ActionView::Template) or
135
+ return nil
136
+ view_paths.find_template('pre' + layout.path_without_format_and_extension, layout.format)
137
+ rescue ActionView::MissingTemplate
138
+ end
139
+ end
140
+
141
+ class StreamingBody
142
+ def initialize(threshold, &block)
143
+ @process = block
144
+ @bytes_to_threshold = threshold
145
+ end
146
+
147
+ def each(&block)
148
+ @push = block
149
+ @process.call
150
+ end
151
+
152
+ def push(data)
153
+ if @bytes_to_threshold > 0
154
+ @push.call(data + padding)
155
+ @bytes_to_threshold = 0
156
+ else
157
+ @push.call(data)
158
+ end
159
+ end
160
+
161
+ private # -------------------------------------------------------
162
+
163
+ def padding
164
+ content_length = [@bytes_to_threshold - 7, 0].max
165
+ "<!--#{'-'*content_length}-->"
166
+ end
167
+ end
168
+
169
+ ActionView::Base.send :include, View
170
+ ActionController::Base.send :include, Controller
171
+ ActionController::Response.send :include, Response
172
+ end
@@ -0,0 +1,3 @@
1
+ module TemplateStreaming
2
+ VERSION = [0, 0, 1]
3
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: template_streaming
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - George Ogata
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-04-13 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: |
33
+ Adds a #flush helper to Rails which lets you flush the output
34
+ buffer to the client early, allowing the client to begin fetching
35
+ external resources while the server is rendering the page.
36
+
37
+ email:
38
+ - george.ogata@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - LICENSE
45
+ - README.markdown
46
+ files:
47
+ - doc/fast-profile.png
48
+ - doc/slow-profile.png
49
+ - lib/template_streaming/version.rb
50
+ - lib/template_streaming.rb
51
+ - LICENSE
52
+ - README.markdown
53
+ - Rakefile
54
+ - CHANGELOG
55
+ has_rdoc: true
56
+ homepage: http://github.com/oggy/template_streaming
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 1
77
+ - 3
78
+ - 6
79
+ version: 1.3.6
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.6
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Rails plugin which enables progressive rendering.
87
+ test_files: []
88
+