Neu: Rails Schulungen in Deutschland vom Autor dieses Buches.div class="para">Beim Page Caching geht es darum, eine komplette HTML-Seite (also das Render-Ergebnis eines Views) in einem Unterverzeichnis des public-Verzeichnisses abzulegen und beim nächsten Aufruf dieser Webseite von dort direkt vom Webserver (z. B. Nginx) ausliefern zu lassen. Zusätzlich kann man auch direkt eine komprimierte gz-Version der HTML-Seite dort abspeichern. Ein Produktiv-Webserver wird Dateien unterhalb von public automatisch selbst ausliefern und kann auch so konfiguriert werden, dass – falls gz-Dateien vorhanden sind – diese direkt ausgeliefert werden.

Bei komplexen Views, die auch mal 500 ms und mehr zum Rendern brauchen, ist die Zeitersparnis natürlich bedeutend. Als Webseitenbetreiber spart man wieder wertvolle Serverresourcen und kann mehr Besucher mit der gleichen Hardware bedienen. Der Besucher der Webseite profitiert durch eine schnellere Auslieferung der Seite.

Warnung

Achten Sie bei der Programmierung Ihrer Rails-Applikation darauf, diese Seite auch selbst upzudaten bzw. zu löschen! Eine Beschreibung dazu finden Sie in „Page Caches automatisch löschen“. Sonst enden Sie später mit einem veralteten Cache.
Achten Sie bitte ebenfalls darauf, dass Page Caching per Default alle URL-Parameter verwirft. So wird die Abfrage http://0.0.0.0:3000/companies?search=abc zu http://0.0.0.0:3000/companies. Das lässt sich aber leicht mit einer besserer Routen-Logik lösen.
Bitte installieren Sie eine frische Beispielapplikation (siehe „Eine einfache Beispielapplikation“).

Page Caching im Development-Modus aktivieren

Als Erstes müssen wir in der Datei config/environments/development.rb den Eintrag config.action_controller.perform_caching auf true setzen:
config.action_controller.perform_caching = true
Sonst können wir das Page Caching im Development-Modus nicht ausprobieren. Im Production-Modus ist Page Caching per Default aktiviert.

Company Index- und Show-View cachen

Page Caching wird im Controller aktiviert. Wenn wir für Company die Index- und Show-Views cachen wollen, dann müssen wir im Controller app/controllers/companies_controller.rb am Kopf den caches_page :index, :show-Befehl eingeben:
class CompaniesController < ApplicationController
  caches_page :index, :show

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
Vor dem Starten der Applikation sieht das public-Verzeichnis so aus:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- favicon.ico
|-- index.html
`-- robots.txt
Nach dem Starten der Applikation mit rails server und dem Abruf der URLs http://0.0.0.0:3000/companies und http://0.0.0.0:3000/companies/1 mit einem Webbrowser sieht es so aus:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- companies
|   `-- 1.html
|-- companies.html
|-- favicon.ico
|-- index.html
`-- robots.txt
Die Dateien public/companies.html und public/companies/1.html wurden vom Page Caching erstellt. Ab sofort wird der Webserver beim Aufruf dieser Seiten nur noch die gecachten Versionen liefern.

gz-Versionen

Wenn man Page Cache einsetzt, dann sollte man auch direkt gezippte gz-Dateien cachen. Das geht mit der Option :gzip => true oder anstatt true einen bestimmten Kompressionsparameter als Symbol (z. B. :best_compression).
Der Controller app/controllers/companies_controller.rb würde dann am Anfang so aussehen:
class CompaniesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
Damit werden automatisch eine komprimierte und eine unkomprimierte Variante eines jeden Page Caches abgespeichert:
public/
|-- 404.html
|-- 422.html
|-- 500.html
|-- companies
|   |-- 1.html
|   `-- 1.html.gz
|-- companies.html
|-- companies.html.gz
|-- favicon.ico
|-- index.html
`-- robots.txt

Die Dateiendung .html

Rails speichert die mit http://0.0.0.0:3000/companies aufgerufene Seite unter dem Dateinamen companies.html. Damit wird der vorgeschaltete Webserver zwar diese Datei beim Aufruf von http://0.0.0.0:3000/companies.html finden und ausliefern, aber nicht beim Aufruf von http://0.0.0.0:3000/companies, denn es fehlt ja das .html am Ende der URI.
Bei der Verwendung des in Kapitel 15, Webserver im Produktionsbetrieb erklärten Nginx-Servers geht das am leichtesten mit der folgenden Anpassung der try_files-Anweisung in der Nginx-Konfiguration-Datei:
try_files $uri/index.html $uri $uri.html @unicorn;
Nginx schaut damit nach, ob eine Datei mit der Endung .html von der aktuell aufgerufenen URI existiert.

Page Caches automatisch löschen

Sobald sich die im View verwendeten Daten verändern, müssen natürlich die gespeicherten Cache-Dateien gelöscht werden. Sonst wäre der Cache nicht mehr aktuell.
Laut offizieller Rails-Doku ist die Lösung für dieses Problem die Klasse ActionController::Caching::Sweeper. Dieser auf http://guides.rubyonrails.org/caching_with_rails.html#sweepers beschriebene Weg hat aber einen großen Nachteil: Er beschränkt sich nur auf Aktionen, die innerhalb des Controllers geschehen. Wenn also eine Action per URL vom Webbrowser getriggert wird, dann wird auch der entsprechende Cache verändert oder gelöscht. Wenn aber z. B. in der Console ein Objekt gelöscht wird, bekommt der Sweeper davon gar nichts mit. Deshalb stelle ich Ihnen hier einen Ansatz vor, der keinen Sweeper benutzt, sondern direkt im Model mit ActiveRecord Callbacks arbeitet.
In unserer Telefonbuchapplikation müssen wir bei der Veränderung einer Firma immer den Cache für http://0.0.0.0:3000/companies und http://0.0.0.0:3000/companies/id_der_firma löschen. Bei einer Veränderung eines Angestellten müssen wir zusätzlich noch die entsprechenden Caches zu dem Angestellten löschen.

Wichtig

Wir müssen darauf achten, keine Seiten mit einer Flash-Nachricht zu cachen. Weiterhin macht es auch keinen Sinn, bei diesen gecacheten Seiten einen CSRF-Meta-Tag einzubauen. Beides wird im folgenden Code beachtet.

Controller

Fangen wir mit den Controllern an. Bitte ändern Sie den Anfang von app/controllers/companies_controller.rb wie folgt ab:
class CompaniesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression, 
                             :if => Proc.new { flash.count == 0 }
  before_filter(only: [:index, :show]) { @page_caching_is_active = true if flash.count == 0 }

  # GET /companies
  # GET /companies.json
  def index
    @companies = Company.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @companies }
    end
  end

[...]
Bitte fügen Sie die cache_page-Anweisung auch in den Controller app/controllers/employees_controller.rb ein:
class EmployeesController < ApplicationController
  caches_page :index, :show, :gzip => :best_compression, 
                             :if => Proc.new { flash.count == 0 }
  before_filter(only: [:index, :show]) { @page_caching_is_active = true if flash.count == 0 }

  # GET /employees
  # GET /employees.json
  def index
    @employees = Employee.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @employees }
    end
  end

[...]

Models

Jetzt müssen wir in den Models noch eintragen, dass die entsprechenden Caches automatisch gelöscht werden, sobald sich ein Objekt erstellt, verändert oder gelöscht wird.
app/models/company.rb
class Company < ActiveRecord::Base
  attr_accessible :name

  validates :name,
            :presence => true,
            :uniqueness => true

  has_many :employees, :dependent => :destroy

  after_create   :expire_cache
  after_update   :expire_cache
  before_destroy :expire_cache

  def to_s
    name
  end

  def expire_cache
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.company_path(self))
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.companies_path)
  end

end
app/models/employee.rb
class Employee < ActiveRecord::Base
  attr_accessible :company_id, :first_name, :last_name, :phone_number

  belongs_to :company, :touch => true

  validates :first_name,
            :presence => true

  validates :last_name,
            :presence => true

  validates :company,
            :presence => true

  after_create   :expire_cache
  after_update   :expire_cache
  before_destroy :expire_cache

  def to_s
    "#{first_name} #{last_name}"
  end

  def expire_cache
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.employee_path(self))
    ActionController::Base.expire_page(Rails.application.routes.url_helpers.employees_path)
    self.company.expire_cache
  end

end

application.html.erb

In der app/views/layouts/application.html.erb müssen wir noch berücksichtigen, ob ein CSRF-Meta-Tag eingebaut wird. Dies ist bei gecacheten Seiten nicht der Fall.
<!DOCTYPE html>
<html>
<head>
  <title>PhoneBook</title>
  <%= stylesheet_link_tag    "application", :media => "all" %>
  <%= javascript_include_tag "application" %>
  <%= csrf_meta_tag unless @page_caching_is_active %>
</head>
<body>

<%= yield %>

</body>
</html>

Autor

Stefan Wintermeyer