Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Render It!: A Deep Dive into ActionView and Template Engines

Akira Matsuda
September 27, 2014

Render It!: A Deep Dive into ActionView and Template Engines

Slides for Rails Pacific 2014 talk "Render It!: A Deep Dive into ActionView and Template Engines" http://railspacific.com/

Akira Matsuda

September 27, 2014
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. Action View Rails <= 4.0: Part of Action Pack Rails

    4.1: Extracted to a separate gem
  2. actionview.gemspec Gem::Specification.new do |s| ... s.add_dependency 'activesupport', version s.add_dependency 'builder',

    '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' s.add_dependency 'rails-html-sanitizer', '~> 1.0' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version end
  3. actionview.gemspec Gem::Specification.new do |s| ... s.add_dependency 'activesupport', version s.add_dependency 'builder',

    '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' s.add_dependency 'rails-html-sanitizer', '~> 1.0' s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version end
  4. Why was ActionView extracted? Rails apps without ActionView Something more

    lightweight, maybe? Apps without rails / actionpack e.g. Sinatra You can use ActionView's rich helper methods View files could be rails compatible
  5. rails --help Options: -r, [--ruby=PATH] # Path to the Ruby

    binary of your choice # Default: /Users/a_matsuda/.rbenv/versions/2.1.3/bin/ruby -m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL) [--skip-gemfile], [--no-skip-gemfile] # Don't create a Gemfile -B, [--skip-bundle], [--no-skip-bundle] # Don't run bundle install -G, [--skip-git], [--no-skip-git] # Skip .gitignore file [--skip-keeps], [--no-skip-keeps] # Skip source control .keep files -O, [--skip-active-record], [--no-skip-active-record] # Skip Active Record files -V, [--skip-action-view], [--no-skip-action-view] # Skip Action View files -S, [--skip-sprockets], [--no-skip-sprockets] # Skip Sprockets files [--skip-spring], [--no-skip-spring] # Don't install Spring application preloader -d, [--database=DATABASE] # Preconfigure for selected database (options: mysql/oracle/postgresql/ sqlite3/frontbase/ibm_db/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc) # Default: sqlite3 -j, [--javascript=JAVASCRIPT] # Preconfigure for selected JavaScript library # Default: jquery -J, [--skip-javascript], [--no-skip-javascript] # Skip JavaScript files [--dev], [--no-dev] # Setup the application with Gemfile pointing to your Rails checkout [--edge], [--no-edge] # Setup the application with Gemfile pointing to Rails repository -T, [--skip-test-unit], [--no-skip-test-unit] # Skip Test::Unit files [--rc=RC] # Path to file containing extra configuration options for rails command [--no-rc], [--no-no-rc] # Skip loading of extra configuration options from .railsrc file
  6. OMG there it is! Options: -r, [--ruby=PATH] # Path to

    the Ruby binary of your choice # Default: /Users/a_matsuda/.rbenv/versions/2.1.3/bin/ruby -m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL) [--skip-gemfile], [--no-skip-gemfile] # Don't create a Gemfile -B, [--skip-bundle], [--no-skip-bundle] # Don't run bundle install -G, [--skip-git], [--no-skip-git] # Skip .gitignore file [--skip-keeps], [--no-skip-keeps] # Skip source control .keep files -O, [--skip-active-record], [--no-skip-active-record] # Skip Active Record files -V, [--skip-action-view], [--no-skip-action-view] # Skip Action View files -S, [--skip-sprockets], [--no-skip-sprockets] # Skip Sprockets files [--skip-spring], [--no-skip-spring] # Don't install Spring application preloader -d, [--database=DATABASE] # Preconfigure for selected database (options: mysql/oracle/postgresql/ sqlite3/frontbase/ibm_db/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc) # Default: sqlite3 -j, [--javascript=JAVASCRIPT] # Preconfigure for selected JavaScript library # Default: jquery -J, [--skip-javascript], [--no-skip-javascript] # Skip JavaScript files [--dev], [--no-dev] # Setup the application with Gemfile pointing to your Rails checkout [--edge], [--no-edge] # Setup the application with Gemfile pointing to Rails repository -T, [--skip-test-unit], [--no-skip-test-unit] # Skip Test::Unit files [--rc=RC] # Path to file containing extra configuration options for rails command [--no-rc], [--no-no-rc] # Skip loading of extra configuration options from .railsrc file
  7. config/application.rb # Pick the frameworks you want: require "active_model/railtie" require

    "active_record/railtie" require "action_controller/railtie" require "action_mailer/railtie" # require "action_view/railtie" require "sprockets/railtie" require "rails/test_unit/railtie"
  8. Let's generate something % rails g scaffold user name %

    rake db:migrate % rails s % open http://localhost:3000/users
  9. config/application.rb # Pick the frameworks you want: require "active_model/railtie" require

    "active_record/railtie" require "action_controller/railtie" require "action_mailer/railtie" # require "action_view/railtie" require "sprockets/railtie" require "rails/test_unit/railtie"
  10. WTF

  11. This should be a bug. Let's just remove this line.

    require "action_dispatch/railtie" require "abstract_controller/railties/routes_helpers" require "action_controller/railties/helpers" -require "action_view/railtie" module ActionController class Railtie < Rails::Railtie #:nodoc:
  12. Because these gems depend on actionview in their gemspecs actionpack

    actionmailer sprockets-rails (rails depends on it)
  13. sprockets-rails/sprockets/rails/ helper.rb require 'action_view' require 'sprockets' require 'active_support/core_ext/class/attribute' module Sprockets

    module Rails module Helper ... include ActionView::Helpers::AssetUrlHelper include ActionView::Helpers::AssetTagHelper ...
  14. Let's try creating a minimum ActionView app A command line

    app that renders a template to STDOUT
  15. % git grep "def render" % git grep "def render"

    actionview/lib dependency_tracker.rb: def render_dependencies helpers/rendering_helper.rb: def render(options = {}, locals = {}, &block) helpers/tags/base.rb: def render helpers/tags/check_box.rb: def render helpers/tags/collection_check_boxes.rb: def render(&block) helpers/tags/collection_check_boxes.rb: def render_component(builder) helpers/tags/collection_helpers.rb: def render_collection #:nodoc: helpers/tags/collection_radio_buttons.rb: def render(&block) helpers/tags/collection_radio_buttons.rb: def render_component(builder) helpers/tags/collection_select.rb: def render helpers/tags/color_field.rb: def render helpers/tags/date_select.rb: def render helpers/tags/datetime_field.rb: def render helpers/tags/grouped_collection_select.rb: def render helpers/tags/label.rb: def render(&block) helpers/tags/label.rb: def render_component(builder) helpers/tags/number_field.rb: def render helpers/tags/password_field.rb: def render helpers/tags/radio_button.rb: def render helpers/tags/search_field.rb: def render helpers/tags/select.rb: def render helpers/tags/text_area.rb: def render helpers/tags/text_field.rb: def render helpers/tags/time_zone_select.rb: def render log_subscriber.rb: def render_template(event) renderer/abstract_renderer.rb: def render renderer/partial_renderer.rb: def render(context, options, block) renderer/partial_renderer.rb: def render_collection renderer/partial_renderer.rb: def render_partial renderer/renderer.rb: def render(context, options) renderer/renderer.rb: def render_body(context, options) renderer/renderer.rb: def render_template(context, options) #:nodoc: renderer/renderer.rb: def render_partial(context, options, &block) #:nodoc: renderer/streaming_template_renderer.rb: def render_template(template, layout_name = nil, locals = {}) #:nodoc: renderer/template_renderer.rb: def render(context, options) renderer/template_renderer.rb: def render_template(template, layout_name = nil, locals = nil) #:nodoc: renderer/template_renderer.rb: def render_with_layout(path, locals) #:nodoc: rendering.rb: def render_to_body(options = {}) rendering.rb: def rendered_format template.rb: def render(view, locals, buffer=nil, &block) template/html.rb: def render(*args) template/text.rb: def render(*args) test_case.rb: def render(options = {}, local_assigns = {}, &block) test_case.rb: def rendered_views test_case.rb: def rendered_views test_case.rb: def render(options = {}, local_assigns = {})
  16. Helpers::Tags and TestCase % git grep "def render" actionview/lib dependency_tracker.rb:

    def render_dependencies helpers/rendering_helper.rb: def render(options = {}, locals = {}, &block) helpers/tags/base.rb: def render helpers/tags/check_box.rb: def render helpers/tags/collection_check_boxes.rb: def render(&block) helpers/tags/collection_check_boxes.rb: def render_component(builder) helpers/tags/collection_helpers.rb: def render_collection #:nodoc: helpers/tags/collection_radio_buttons.rb: def render(&block) helpers/tags/collection_radio_buttons.rb: def render_component(builder) helpers/tags/collection_select.rb: def render helpers/tags/color_field.rb: def render helpers/tags/date_select.rb: def render helpers/tags/datetime_field.rb: def render helpers/tags/grouped_collection_select.rb: def render helpers/tags/label.rb: def render(&block) helpers/tags/label.rb: def render_component(builder) helpers/tags/number_field.rb: def render helpers/tags/password_field.rb: def render helpers/tags/radio_button.rb: def render helpers/tags/search_field.rb: def render helpers/tags/select.rb: def render helpers/tags/text_area.rb: def render helpers/tags/text_field.rb: def render helpers/tags/time_zone_select.rb: def render log_subscriber.rb: def render_template(event) renderer/abstract_renderer.rb: def render renderer/partial_renderer.rb: def render(context, options, block) renderer/partial_renderer.rb: def render_collection renderer/partial_renderer.rb: def render_partial renderer/renderer.rb: def render(context, options) renderer/renderer.rb: def render_body(context, options) renderer/renderer.rb: def render_template(context, options) #:nodoc: renderer/renderer.rb: def render_partial(context, options, &block) #:nodoc: renderer/streaming_template_renderer.rb: def render_template(template, layout_name = nil, locals = {}) #:nodoc: renderer/template_renderer.rb: def render(context, options) renderer/template_renderer.rb: def render_template(template, layout_name = nil, locals = nil) #:nodoc: renderer/template_renderer.rb: def render_with_layout(path, locals) #:nodoc: rendering.rb: def render_to_body(options = {}) rendering.rb: def rendered_format template.rb: def render(view, locals, buffer=nil, &block) template/html.rb: def render(*args) template/text.rb: def render(*args) test_case.rb: def render(options = {}, local_assigns = {}, &block) test_case.rb: def rendered_views test_case.rb: def rendered_views test_case.rb: def render(options = {}, local_assigns = {})
  17. Excluding Helpers::Tags and TestCase % git grep "def render" actionview/lib

    | grep -v "^helpers\/tags\/" | grep -v "^test_case\.rb" dependency_tracker.rb: def render_dependencies helpers/rendering_helper.rb: def render(options = {}, locals = {}, &block) log_subscriber.rb: def render_template(event) renderer/abstract_renderer.rb: def render renderer/partial_renderer.rb: def render(context, options, block) renderer/partial_renderer.rb: def render_collection renderer/partial_renderer.rb: def render_partial renderer/renderer.rb: def render(context, options) renderer/renderer.rb: def render_body(context, options) renderer/renderer.rb: def render_template(context, options) #:nodoc: renderer/renderer.rb: def render_partial(context, options, &block) #:nodoc: renderer/streaming_template_renderer.rb: def render_template(template, layout_name = nil, locals = {}) #:nodoc: renderer/template_renderer.rb: def render(context, options) renderer/template_renderer.rb: def render_template(template, layout_name = nil, locals = nil) #:nodoc: renderer/template_renderer.rb: def render_with_layout(path, locals) #:nodoc: rendering.rb: def render_to_body(options = {}) rendering.rb: def rendered_format template.rb: def render(view, locals, buffer=nil, &block) template/html.rb: def render(*args) template/text.rb: def render(*args)
  18. Error! % bundle ex ruby app.rb action_view/lookup_context.rb:69:in `get': uninitialized constant

    ActionView::LookupContext::DetailsKey::Mime (NameError) from action_view/lookup_context.rb:90:in `details_key' from action_view/lookup_context.rb:158:in `detail_args_for' from action_view/lookup_context.rb:152:in `args_for_lookup' from action_view/lookup_context.rb:121:in `find' from action_view/renderer/abstract_renderer.rb:18:in `find_template' from action_view/renderer/template_renderer.rb:32:in `block in determine_template' from action_view/lookup_context.rb:143:in `with_fallbacks' from action_view/renderer/abstract_renderer.rb:18:in `with_fallbacks' from action_view/renderer/template_renderer.rb:32:in `determine_template' from action_view/renderer/template_renderer.rb:8:in `render' from action_view/renderer/renderer.rb:42:in `render_template' from action_view/renderer/renderer.rb:23:in `render' from action_view/helpers/rendering_helper.rb:32:in `render' from app.rb:2:in `<main>'
  19. s.add_development_dependency 'actionpack' Uses actionpack only for testing Doesn't require actionpack

    in runtime I'm sorry I told you a lie! actionview still depends on actionpack in runtime!
  20. Jeremy said, :+1: it should have a runtime dependency. It's

    still closely coupled with Action Pack.
  21. Apps without rails / actionpack It turned out this was

    impossible At least, I'm sure nobody has tried this before
  22. app.rb - Workaround Bundler.require # copied from AD/http/mime_type.rb (and cut

    the last line) require_relative 'mime_type' puts ActionView::Base.new.render(file: 'hello.txt') #=> hello
  23. Error! % bundle ex ruby app.rb action_view/path_set.rb:46:in `find': Missing template

    /hello.html.erb with {:locale=>[:en], :formats=>[:html, :text, :js, :css, :xml, :json], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby]}. Searched in: (ActionView::MissingTemplate) from action_view/lookup_context.rb:121:in `find' from action_view/renderer/abstract_renderer.rb:18:in `find_template' from action_view/renderer/template_renderer.rb:40:in `determine_template' from action_view/renderer/template_renderer.rb:8:in `render' from action_view/renderer/renderer.rb:42:in `render_template' from action_view/renderer/renderer.rb:23:in `render' from action_view/helpers/rendering_helper.rb:32:in `render' from app.rb:13:in `<main>'
  24. app.rb - render :template Bundler.require require_relative 'mime_type' # need to

    give a view_path puts ActionView::Base.new('.').render(template: 'hello.html.erb') #=> hello, erb!
  25. render template: 'hello' No need to specify ".html.erb" actionview would

    find a template having suitable format and handler (and locale and variants)
  26. app.rb - render :template Bundler.require require_relative 'mime_type' # no need

    to specify ".html.erb" puts ActionView::Base.new('.').render(template: 'hello') #=> hello, erb!
  27. How does the actual Rails controller render? % rails new

    someapp % cd someapp % rails g controller welcome index % echo "<% puts caller %>" >> app/views/ welcome/index.html.erb % rails s % open http://localhost:3000/welcome/index
  28. The callers actionview/lib/action_view/template.rb:145:in `block in render' activesupport/lib/active_support/notifications.rb:166:in `instrument' actionview/lib/action_view/template.rb:329:in `instrument'

    actionview/lib/action_view/template.rb:143:in `render' actionview/lib/action_view/renderer/template_renderer.rb:54:in `block (2 levels) in render_template' actionview/lib/action_view/renderer/abstract_renderer.rb:39:in `block in instrument' activesupport/lib/active_support/notifications.rb:164:in `block in instrument' activesupport/lib/active_support/notifications/instrumenter.rb:20:in `instrument' activesupport/lib/active_support/notifications.rb:164:in `instrument' actionview/lib/action_view/renderer/abstract_renderer.rb:39:in `instrument' actionview/lib/action_view/renderer/template_renderer.rb:53:in `block in render_template' actionview/lib/action_view/renderer/template_renderer.rb:61:in `render_with_layout' actionview/lib/action_view/renderer/template_renderer.rb:52:in `render_template' actionview/lib/action_view/renderer/template_renderer.rb:14:in `render' actionview/lib/action_view/renderer/renderer.rb:42:in `render_template' actionview/lib/action_view/renderer/renderer.rb:23:in `render' actionview/lib/action_view/rendering.rb:100:in `_render_template' actionpack/lib/action_controller/metal/streaming.rb:217:in `_render_template' actionview/lib/action_view/rendering.rb:83:in `render_to_body' actionpack/lib/action_controller/metal/rendering.rb:32:in `render_to_body' actionpack/lib/action_controller/metal/renderers.rb:37:in `render_to_body' actionpack/lib/abstract_controller/rendering.rb:25:in `render'
  29. What is `view_context`? module ActionView module Rendering def view_context view_context_class.new(view_renderer,

    view_assigns, self) end def view_context_class @_view_context_class ||= self.class.view_context_class ennnd
  30. What is `view_context`? module ActionView module Rendering module ClassMethods def

    view_context_class @view_context_class ||= begin include_path_helpers = supports_path? routes = respond_to?(:_routes) && _routes helpers = respond_to?(:_helpers) && _helpers Class.new(ActionView::Base) do if routes include routes.url_helpers(include_path_helpers) include routes.mounted_helpers end if helpers include helpers ennnnnnnd
  31. `view_context` is not a direct instance of ActionView::Base class an

    instance of an anonymous child class of ActionView::Base
  32. What is `view_renderer`? module ActionView class Base attr_accessor :view_renderer def

    initialize(context = nil, assigns = {}, controller = nil, formats = nil) ... @view_renderer = ActionView::Renderer.new(lookup_context) ... ennnd
  33. renderer/template_renderer.rb module ActionView class TemplateRenderer < AbstractRenderer def render(context, options)

    @view = context ... template = determine_template(options) ... render_template(template, options[:layout], options[:locals]) ennnd
  34. Then another render_template after another render after render_template after render.

    This render method determines a template, then renders it
  35. renderer/template_renderer.rb module ActionView class TemplateRenderer < AbstractRenderer def determine_template(options) ...

    elsif options.key?(:text) Template::Text.new(options[:text], formats.first) ... elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) elsif options.key?(:template) ... find_template(options[:template], options[:prefixes], false, keys, @details) ennnnnd
  36. template/text.rb module ActionView class Template class Text def initialize(string, type

    = nil) @string = string.to_s ... end def to_str @string end def render(*args) to_str ennnnd
  37. template/resolver.rb handler, format, variant = extract_handler_and_format_and_variant(template, formats) contents = File.binread(template)

    Template.new(contents, File.expand_path(template), handler, :virtual_path => path.virtual, :format => format, :variant => variant, :updated_at => mtime(template) )
  38. Template resolution Read the content from the file Create a

    handler instance Return an instance of Template that has the content and the handler
  39. renderer/template_renderer.rb module ActionView class TemplateRenderer < AbstractRenderer def render_template(template, layout_name

    = nil, locals = nil) ... template.render(view, locals) { |*name| view._layout_for(*name) } end
  40. template.rb module ActionView class Template def render(view, locals, buffer=nil, &block)

    ... compile!(view) view.send(method_name, locals, buffer, &block) ... ennnd
  41. template.rb module ActionView class Template def compile!(view) return if @compiled

    ... mod = view.singleton_class ... compile(mod) ... ennnd
  42. template.rb module ActionView class Template def compile(mod) ... method_name =

    self.method_name code = @handler.call(self) ... source = <<-end_src def #{method_name}(local_assigns, output_buffer) _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} ensure @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer end end_src ... mod.module_eval(source, identifier, 0) ... ennnd
  43. template.rb module ActionView class Template def render(view, locals, buffer=nil, &block)

    ... compile!(view) view.send(method_name, locals, buffer, &block) ... ennnd
  44. Back to `render` Then the defined method is called Template

    rendering is not `eval` but a method execution
  45. Template Handler API A template handler has to respond_to #call

    that takes a Template instance #call returns a compiled HTML String
  46. app.rb - confirming that a method was added Bundler.require require_relative

    'mime_type' view = Class.new(ActionView::Base).new('.'). methods = view.public_methods view.render(template: 'hello') p view.public_methods - methods #=> [:___sers_a_matsuda_tmp_actionview_test_hello_html_erb___149 3524731793660833_70284245975760]
  47. ERB

  48. template/handlers/erb.rb module ActionView class Template module Handlers class ERB class_attribute

    :erb_implementation self.erb_implementation = Erubis def call(template) ... self.class.erb_implementation.new( erb, :escape => (self.class.escape_whitelist.include? template.type), :trim => (self.class.erb_trim_mode == "-") ).src end
  49. self.erb_implementation = Erubis Erubis is an ERB compatible eRuby library

    Looks as if erb_implementation is configurable
  50. self.erb_implementation = ERB (?) But if you set this to

    `ERB` the actionview tests will never pass. Here's another unknown "meaningless option". Seki-san and I once tried to fix this before Ruby 2.0 release, but we failed... Weird.
  51. ERB.new(erb).src % ruby -rerb -e "puts ERB.new('<%= 1 + 1

    %>').src" #coding:UTF-8 _erbout = ''; _erbout.concat(( 1 + 1 ).to_s); _erbout.force_encoding(__ENCODING__)
  52. ERB.new(erb).src It gets an ERB template string and returns a

    ruby code that generates the result HTML
  53. app.rb - rendering haml Bundler.require require_relative 'mime_type' require 'haml/template' view

    = Class.new(ActionView::Base).new('.') puts view.render(template: 'hello', handlers: 'haml')
  54. app.rb - rendering haml with weird monkey-patches Bundler.require require_relative 'mime_type'

    module ActionPack module VERSION MAJOR = 4 end end module Rails def self.env ActiveSupport::StringInquirer.new('production') end end require 'haml/template' view = Class.new(ActionView::Base).new('.') puts view.render(template: 'hello', handlers: 'haml')
  55. app.rb - rendering slim with weird monkey-patches Bundler.require require_relative 'mime_type'

    module ActionPack module VERSION MAJOR = 4 end end module Rails def self.env ActiveSupport::StringInquirer.new('production') end end require 'slim' view = Class.new(ActionView::Base).new('.') puts view.render(template: 'hello', handlers: 'slim')
  56. Benchmark! Bundler.require require_relative 'mime_type' module ActionPack module VERSION MAJOR =

    4 end end module Rails def self.env ActiveSupport::StringInquirer.new('production') end end require 'haml/template' require 'slim' view = Class.new(ActionView::Base).new('.') # discard the first execution that contains template lookup and compilation view.render(template: 'hello', handlers: 'erb') view.render(template: 'hello', handlers: 'haml') view.render(template: 'hello', handlers: 'slim') Benchmark.ips do |x| x.report('erb') { view.render(template: 'hello', handlers: 'erb') } x.report('haml') { view.render(template: 'hello', handlers: 'haml') } x.report('slim') { view.render(template: 'hello', handlers: 'slim') } end
  57. Benchmark results % bundle ex ruby app.rb Calculating ------------------------------------- erb

    1839 i/100ms haml 1440 i/100ms slim 1920 i/100ms ------------------------------------------------- erb 19261.7 (±4.2%) i/s - 97467 in 5.069805s haml 14662.8 (±5.0%) i/s - 73440 in 5.022658s slim 19673.9 (±3.6%) i/s - 99840 in 5.081665s
  58. Results % bx ruby app.rb Calculating ------------------------------------- erb 1824 i/100ms

    haml(gem) 1440 i/100ms haml(fork) 1553 i/100ms slim 1875 i/100ms ------------------------------------------------- erb 19614.9 (±3.4%) i/s - 98496 in 5.027551s haml(gem) 14662.8 (±5.0%) i/s - 73440 in 5.022658s haml(fork) 16193.5 (±4.2%) i/s - 82309 in 5.092455s slim 20193.5 (±3.5%) i/s - 101250 in 5.020527s
  59. end