#169 Dynamic Page Caching (revised)
- Download:
- source codeProject Files in Zip (109 KB)
- mp4Full Size H.264 Video (14.9 MB)
- m4vSmaller H.264 Video (8.57 MB)
- webmFull Size VP8 Video (10.9 MB)
- ogvFull Size Theora Video (18 MB)
제품을 관리하는 웹어플리케이션의 페이지가 아래에 있습니다.
이 페이지는 실행하는데 약간의 시간이 소요되는데 레일스가 스케일링을 하지 않아서가 아니라, 요청이 지연되는 것을 시뮬레이션하기 위해서 2초동안 sleep 하도록 index
액션에 코딩작업을 해 놓았기 때문입니다.
def index @products = Product.all sleep 2 end
이와 같이 특정 페이지의 로딩속도가 늦을 경우에, 페이지 캐싱을 이용해서, 최근 갱신된 연재에 보여준 바와 같이, 로딩속도를 빠르게 할 수 있습니다.
class ProductsController < ApplicationController caches_page :index def index @products = Product.all sleep 2 end # Other actions omitted. end
개발모드에서 페이지 캐싱을 사용하기 위해서는 config 파일에서 관련 옵션을 변경할 필요가 있습니다.
config.action_controller.perform_caching = true
We’ll need to restart the server for this change to take effect. After we’ve done this that page will still load slowly the next time we request it but after that it will be very quick as it’s being served by the cache. This works by saving the HTML output from the action to a static file under the application’s /public
directory but we have a problem with this approach here in that there’s some dynamic content on the page that’s specific to the current user. At the top of the page their login status is shown and now that we’ve cached the page while being logged in as the admin
user anyone else who visits the page will see the “Logged in as admin.” status message. Even if we log out from the app this message will persist as it’s statically cached.
We could work around this by looking at different types of caching, for example fragment caching, but this will also be an issue as we have “edit” and “destroy” links next to each product and these should only show when the current user is an admin. An alternative solution is to stick with page caching but hide all the dynamic content then load it back in as appropriate through JavaScript. We’ll show you how to do just that in this episode.
Adding Dynamic Page Caching
First we’ll disable the caching we’ve just added and remove the sleep
from the index
action so that we can focus on getting everything working.
class ProductsController < ApplicationController #caches_page :index def index @products = Product.all #sleep 2 end # Other actions omitted. end
We’ll also remove the cached file at /public/index.html
. Now when we reload the page it falls back to its proper logged-out status and hides all the administration links for managing the products.
Next we’ll need to modify this section of view template for this page.
<% if admin? %> <div class="admin"> <%= link_to "Edit", edit_product_path(product) %> | <%= link_to "Destroy", product, method: "delete", confirm: "Are you sure?" %> </div> <% end %>
We’ll need to remove the code that only shows the admin links if the current user is an admin so that they’re included on the page all the time.
<div class="admin"> <%= link_to "Edit", edit_product_path(product) %> | <%= link_to "Destroy", product, method: "delete", confirm: "Are you sure?" %> </div>
We’ll then stop the links from being displayed with some CSS.
// Place all the styles related to the products controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ #products .admin { display: none; }
When we visit the page now the links are there in the source but they’re not displayed. If we’re logged in as an admin we want the links to be displayed through JavaScript. We’ll do this by adding some code to the products CoffeeScript file.
jQuery -> if $('#products').length $.getScript('/users/current')
This code first checks that the page’s DOM has loaded. Once it has we make sure that we’re on the right page by looking for the products
div. The JavaScript doesn’t know the current user’s status and so we need to make a request back to the server and we use getScript
for this. This calls an action on the server and as we’re using the path /users/current
the UsersController
’s show
action will be called, although we can use any action we want. We’ll write that action now and it can be left blank.
def show end
Along with this blank action we’ll create a new JavaScript template.
<% if admin? %> $('.admin').show(); <% end %>
If the current user is an admin this template will return some jQuery code that will show the admin links. This code will be evaluated on the client as a result of the getScript
call in the products CoffeeScript file. Now when we’re logged in as an admin we’ll see the links, but if we log out they won’t be shown.
Handling The Rest of The Dynamic Content
Next we need to handle the rest of the content on the page: the flash message and the user status section. These appear next to each other inside the application’s layout file at the top of the container
div.
<!DOCTYPE html> <html> <head> <title>Store</title> <%= stylesheet_link_tag "application", media: "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> </head> <body> <div id="container"> <div id="user_status"> <% if current_user %> Logged in as <strong><%= current_user.name %></strong>. <%= link_to "Log out", logout_path %> <% else %> <%= link_to "Sign up", signup_path %> or <%= link_to "log in", login_path %> <% end %> </div> <% flash.each do |name, msg| %> <%= content_tag :div, msg, id: "flash_#{name}" %> <% end %> <%= yield %> </div> </body> </html>
The first step here is to move the status and flash message code into a partial that we’ll call dynamic_header
. We want to hide this dynamic content if we’re using page caching and so we won’t render it if a @page_caching
instance variable is set to true
.
<!DOCTYPE html> <html> <head> <title>Store</title> <%= stylesheet_link_tag "application", media: "all" %> <%= javascript_include_tag "application" %> <%= csrf_meta_tag %> </head> <body> <div id="container"> <%= render 'layouts/dynamic_header' unless @page_caching %> <%= yield %> </div> </body> </html>
We have to take this instance variable approach as there’s no easy way to detect whether a given action has page caching enabled. This means that we’ll need to set it in every action where we’re caching the page.
def index @page_caching = true @products = Product.all #sleep 2 end
Now that we’ve hidden the header it’s time to show it again with some JavaScript. We’ll do this in the same show JavaScript template where we display the “edit” and “destroy” links.
<% if admin? %> $('.admin').show(); <% end %> $('#container').prepend('<%= j render("layouts/dynamic_header") %>');
To do this we render out the partial and add it at the start of the container
div. Note that we use the j
method to make sure that the contents of the partial are escaped correctly for JavaScript. With this in place we can reenable page caching and also the sleep
so that we can see it in action.
class ProductsController < ApplicationController caches_page :index def index @products = Product.all sleep 2 end # Other actions omitted. end
When we reload the page now we’ll see the delay, but when we reload it again it will load almost instantly as we’ll be seeing the cached version. When we login, the status and flash messages both update and the “edit” and “destroy” links are shown even though the page is cached.
The performance of this won’t be as great as pure page caching as we’re still sending a separate request to our Rails application to fetch the user-specific content. This content will generally be small enough for the request to return quickly and so this won’t be too much of an issue. If this does become an issue we could cache these details on the client in a cookie when the user logs in. This would remove the need to send a separate request to determine the user’s admin status, name and so on to display them and we can set them call on the client depending on the contents of the cookie. This approach is outside of the scope of this episode but it’s worth investigating if performance is an issue.
We’ll finish off by mentioning one more piece of dynamic content: the crsf_meta_tag
. It’s a good idea to hide this when we’re using page caching so that any cached pages don’t contain that user-specific detail.
<%= csrf_meta_tag unless @page_caching %>