Slide 1

Slide 1 text

A Deep Dive into ActionView and Template Engines Render It! Akira Matsuda

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

begin

Slide 6

Slide 6 text

Contents Part 1: Action View Part 2: Template Engines

Slide 7

Slide 7 text

Part 1: Action View

Slide 8

Slide 8 text

Action View The view layer of Ruby on Rails framework

Slide 9

Slide 9 text

Action View Rails <= 4.0: Part of Action Pack Rails 4.1: Extracted to a separate gem

Slide 10

Slide 10 text

Names Documented name: Action View Gem name: actionview Directory name: action_view Module name: ActionView

Slide 11

Slide 11 text

The gemspec

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

actionview's dependencies actionview is not depending on rails

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

s.add_development_dependency 'actionpack' Uses actionpack only for its testing Doesn't require actionpack in runtime

Slide 16

Slide 16 text

Why was ActionView extracted? Any usecase?

Slide 17

Slide 17 text

Why was ActionView extracted? Rails apps without ActionView Something more lightweight, maybe?

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Why was ActionView extracted? Rails apps without ActionView Apps without rails / actionpack

Slide 20

Slide 20 text

Rails apps without ActionView

Slide 21

Slide 21 text

Rails apps without ActionView Let's make sure if this is possible.

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

% rails new no_actionview -V

Slide 25

Slide 25 text

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"

Slide 26

Slide 26 text

OMG my app is actionview free! But how does it render?

Slide 27

Slide 27 text

Let's generate something % rails g scaffold user name % rake db:migrate % rails s % open http://localhost:3000/users

Slide 28

Slide 28 text

Apparently it renders the view...

Slide 29

Slide 29 text

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"

Slide 30

Slide 30 text

actionpack/lib/action_controller/ railtie.rb require "rails" require "action_controller" require "action_dispatch/railtie" require "abstract_controller/railties/ routes_helpers" require "action_controller/railties/helpers" require "action_view/railtie"

Slide 31

Slide 31 text

WTF

Slide 32

Slide 32 text

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:

Slide 33

Slide 33 text

That's it! Is everything OK?

Slide 34

Slide 34 text

Unfortunately, no.

Slide 35

Slide 35 text

Because these gems depend on actionview in their gemspecs actionpack actionmailer sprockets-rails (rails depends on it)

Slide 36

Slide 36 text

And they touch ActionView code casually. For example,

Slide 37

Slide 37 text

actionpack/lib/abstract_controller/ rendering.rb module AbstractController module Rendering extend ActiveSupport::Concern include ActionView::ViewPaths ennd

Slide 38

Slide 38 text

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 ...

Slide 39

Slide 39 text

You can not skip loading actionview --skip-action-view option was totally meaningless

Slide 40

Slide 40 text

So, I wrote an issue 3.days.ago

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

Jeremy said, :+1: to remove `skip-action-view`

Slide 43

Slide 43 text

So, the option was removed --skip-action-view option was only available in Rails 4.1 Will be removed since Rails 4.2

Slide 44

Slide 44 text

Why was ActionView extracted? Rails apps without ActionView Apps without rails / actionpack

Slide 45

Slide 45 text

Apps without rails / actionpack

Slide 46

Slide 46 text

Let's try creating a minimum ActionView app A command line app that renders a template to STDOUT

Slide 47

Slide 47 text

render :text Let's start off with a very simple case

Slide 48

Slide 48 text

How can I render? Maybe there's a method named "render" inside actionview?

Slide 49

Slide 49 text

% 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 = {})

Slide 50

Slide 50 text

% git grep "def render" 46 render-ish methods inside actionview!

Slide 51

Slide 51 text

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 = {})

Slide 52

Slide 52 text

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)

Slide 53

Slide 53 text

Excluding Helpers::Tags and TestCase Still 20 render methods... No matter which one, view should respond_to `render`

Slide 54

Slide 54 text

Maybe something like this should work?

Slide 55

Slide 55 text

actionview_app/Gemfile source 'https://rubygems.org/' gem 'actionview', require: 'action_view'

Slide 56

Slide 56 text

app.rb - render :text Bundler.require puts ActionView::Base.new.render(text: 'hello')

Slide 57

Slide 57 text

app.rb - render :text Bundler.require puts ActionView::Base.new.render(text: 'hello') #=> hello

Slide 58

Slide 58 text

OMG it worked!

Slide 59

Slide 59 text

render :inline Rendering an inline ERB string

Slide 60

Slide 60 text

Rendering an inline ERB template Bundler.require puts ActionView::Base.new.render(inline: '<%= "hello" %>') #=> hello

Slide 61

Slide 61 text

render :file Finding a template from the filesystem

Slide 62

Slide 62 text

hello.txt % echo hello > hello.txt

Slide 63

Slide 63 text

app.rb - render :file Bundler.require puts ActionView::Base.new.render(file: 'hello.txt')

Slide 64

Slide 64 text

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 `'

Slide 65

Slide 65 text

What is Mime? ::Mime is a module defined in actionpack/lib/action_dispatch/http/mime_type.rb

Slide 66

Slide 66 text

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!

Slide 67

Slide 67 text

So, I wrote an issue 3.days.ago

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

Jeremy said, :+1: it should have a runtime dependency. It's still closely coupled with Action Pack.

Slide 70

Slide 70 text

Apps without rails / actionpack It turned out this was impossible At least, I'm sure nobody has tried this before

Slide 71

Slide 71 text

Why was ActionView extracted? Rails apps without ActionView Apps without rails / actionpack

Slide 72

Slide 72 text

Why was ActionView extracted?

Slide 73

Slide 73 text

Why was ActionView extracted? ¯\_(π)_/¯

Slide 74

Slide 74 text

ProTip: How to find Rails bugs Try something weird on Rails like this

Slide 75

Slide 75 text

Workaround % cp AD/http/mime_type.rb .

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

render :template

Slide 78

Slide 78 text

hello.html.erb % echo "<%= 'hello, erb!' %>" > hello.html.erb

Slide 79

Slide 79 text

app.rb - render :template Bundler.require require_relative 'mime_type' puts ActionView::Base.new.render(template: 'hello.html.erb')

Slide 80

Slide 80 text

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 `'

Slide 81

Slide 81 text

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!

Slide 82

Slide 82 text

render template: 'hello' No need to specify ".html.erb" actionview would find a template having suitable format and handler (and locale and variants)

Slide 83

Slide 83 text

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!

Slide 84

Slide 84 text

What does render :template do? Time for code reading!

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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'

Slide 87

Slide 87 text

rendering.rb module ActionView module Rendering def _render_template(options) ... view_renderer.render(view_context, options) ennnd

Slide 88

Slide 88 text

_render_template It calls `view_renderer`'s `render` method that takes a `view_context`

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

`view_context` is not a direct instance of ActionView::Base class an instance of an anonymous child class of ActionView::Base

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

`view_renderer` is an instance of ActionView::Renderer

Slide 94

Slide 94 text

Back to rendering.rb module ActionView module Rendering def _render_template(options) ... view_renderer.render(view_context, options) ennnd

Slide 95

Slide 95 text

render,

Slide 96

Slide 96 text

renderer/renderer.rb module ActionView class Renderer def render(context, options) if options.key?(:partial) ... else render_template(context, options) ennnnd

Slide 97

Slide 97 text

render_template after render,

Slide 98

Slide 98 text

renderer/renderer.rb module ActionView class Renderer def render_template(context, options) ... TemplateRenderer.new(@lookup_context).render(context, options) ennnd

Slide 99

Slide 99 text

Another render after render_template after render,

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Then another render_template after another render after render_template after render. This render method determines a template, then renders it

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

render :text

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

render :template

Slide 106

Slide 106 text

find_template Very very long journey LookupContext#find => PathSet#find => Resolver#find_all Finally finds a template Templates found are cached

Slide 107

Slide 107 text

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) )

Slide 108

Slide 108 text

Template resolution Read the content from the file Create a handler instance Return an instance of Template that has the content and the handler

Slide 109

Slide 109 text

Handler Template Engines' interface handler_for_extension('erb') #=> ActionView::Template::Handlers::Erubis

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

A template can render itself

Slide 112

Slide 112 text

template.rb module ActionView class Template def render(view, locals, buffer=nil, &block) ... compile!(view) view.send(method_name, locals, buffer, &block) ... ennnd

Slide 113

Slide 113 text

Template#render It compiles the view And executes something on the view object

Slide 114

Slide 114 text

template.rb module ActionView class Template def compile!(view) return if @compiled ... mod = view.singleton_class ... compile(mod) ... ennnd

Slide 115

Slide 115 text

compile! compile is executed only once

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

compile @handler.call(template) returns a Ruby code The code is defined on the view context object as a Ruby method

Slide 118

Slide 118 text

template.rb module ActionView class Template def render(view, locals, buffer=nil, &block) ... compile!(view) view.send(method_name, locals, buffer, &block) ... ennnd

Slide 119

Slide 119 text

Back to `render` Then the defined method is called Template rendering is not `eval` but a method execution

Slide 120

Slide 120 text

Template Handler API A template handler has to respond_to #call that takes a Template instance #call returns a compiled HTML String

Slide 121

Slide 121 text

app.rb - subclassing ActionView::Base Bundler.require require_relative 'mime_type' Class.new(ActionView::Base).new('.').render(template: 'hello')

Slide 122

Slide 122 text

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]

Slide 123

Slide 123 text

Part 2: Template Engines

Slide 124

Slide 124 text

Handler#call Let's take a look at actual implementations

Slide 125

Slide 125 text

Template Engines ERB Haml Slim

Slide 126

Slide 126 text

ERB

Slide 127

Slide 127 text

ERB ERB handler is bundled in actionview by default

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

self.erb_implementation = Erubis Erubis is an ERB compatible eRuby library Looks as if erb_implementation is configurable

Slide 130

Slide 130 text

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.

Slide 131

Slide 131 text

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__)

Slide 132

Slide 132 text

ERB.new(erb).src It gets an ERB template string and returns a ruby code that generates the result HTML

Slide 133

Slide 133 text

Haml

Slide 134

Slide 134 text

Gemfile source 'https://rubygems.org/' gem 'actionview', require: 'action_view' gem 'haml'

Slide 135

Slide 135 text

hello.html.haml % echo "= 'hello, haml'" > hello.html.haml

Slide 136

Slide 136 text

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')

Slide 137

Slide 137 text

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')

Slide 138

Slide 138 text

Slim

Slide 139

Slide 139 text

Gemfile source 'https://rubygems.org/' gem 'actionview', require: 'action_view' gem 'haml' gem 'slim', require: false

Slide 140

Slide 140 text

hello.html.slim % echo "= 'hello, slim'" > hello.html.slim

Slide 141

Slide 141 text

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')

Slide 142

Slide 142 text

Template Engines Which engine is fast?

Slide 143

Slide 143 text

Benchmark!

Slide 144

Slide 144 text

Gemfile source 'https://rubygems.org/' gem 'actionview', path: '~/src/rails/actionview', require: 'action_view' gem 'haml' gem 'slim', require: false gem 'benchmark-ips'

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

Haml is so slow!

Slide 148

Slide 148 text

No content

Slide 149

Slide 149 text

No content

Slide 150

Slide 150 text

So, I tried to improve the speed a little bit

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

end