真实世界中的Rails,第2部分:高级页面缓存

使用 JavaScript. 和 cookies 扩展页面缓存

简介:通常,与用户相关的内容不适于使用页面缓存,原因是针对每个用户的内容会有细微的不同。 通过 JavaScript. 和 cookies,甚至可以在显示某些自定义用户数据时采用页面缓存。本文将研究 Ruby on Rails 中的高级页面缓存。

有了页面缓存,Rails 就可以不再介入。在某种程度上,这是件好事 ,因为您的确可以获得优秀的性能。Rails 只需创建 HTML 页面,将其放入目录,之后,就可以置之于脑 后。从那时起,就由应用服务器管理这些页面,且页面进入应用服务器无需任何循环。从性能的角度而言 ,页面缓存真是天赐之福。

我也钟爱页面缓存,Rails 使之简单利落。只需使用一行代码就可以 启用缓存。如果再加入一些代码,就能通过简单地删除文件操作或使用 Rails 较高层的 API 终止缓存。 这里存在一个问题。并不是每个网站都能使用页面缓存。如果页面上的数据会根据访问它的用户而改变, 那么就不能进行页面缓存。而且,如果很难判断页面何时到期终止,就会发现页面缓存的要求太过苛刻。

比如,几乎在每个页面上,ChangingThePresent.org(参阅 侧栏)都有某些用户数据是根据当前 登录的用户而变化的。图 1 显示了我们最新主页的一部分。(我们一直在努力完善它,所以它有可能会 改变。)这个页面呈现出的问题相对简单。如果能判断用户是否已经登录,就可以用 Flash、JavaScript. 、DHTML 或任何其他基于浏览器的代码动态定制视图。您会发现已登录的用户可以登出系统或查看其配置 文件,而已登出的用户则可以注册或再次登录。

图 1. ChangingThePresent.org 上的登录和登出 视图

图 2 显示了稍微有些高级的用户数据视图,我们的站点就使用了这个视图。图 2 中的两个视图有极大的不 同。为了处理页面缓存,我必须先解决所有的差异。对于每个已登录的用户,我都必须替换掉页面的登出 内容,使之显示登录用户的登录 ID 和用户图片。缓存这些内容会带来另一层面的挑战,因为每个用户的 数据都不尽相同。

图 2. 两个截然不同的视图

这种情况并非 ChangingThePresent.org 所独有。如果需要个性化用户体验,那么不可修改的 Rails 页面缓存的使用就 会受到限制。但如果定制不多,那么实际上还是能很容易地缓存这些页面的。

解决这些问题的方 法很多。我更倾向于使用如下这些技巧:

在 Rails 框架的约束之内,取消页面缓存并使用段缓 存替代它。

先加载页面的大部分,然后使用 JavaScript. 和 Ajax 加载该页面较小的动态部分。 服务器端代码可以检测用户是否登录,然后用 Ajax 呈现合适的部分。

将某些用户状态(比如用 户是否已登录)存储在客户端的 cookie 中。然后,根据 cookie 的内容,使用 JavaScript. 动态更改页 面的外观。

在这三种技巧中,我更喜欢第三种,因为第一和第二种技巧都会将 Rails 应用程序 牵扯进来。要获得最大限度的可伸缩性,就要尽量多地使用静态内容。在本文中,我会侧重于介绍第三种 方式。请不要使用该方法存储任何不能丢失的敏感数据,比如 ICBM 启动代码或信用卡号。对于我们所处 理的这些有限的数据而言,这种方法效果很好。

使用 Show and tell 还是 hide and seek?

在我刚开始试着缓存这个主页时,我本可以简单地用 JavaScript. 替换这些链接。可以将这种技 巧看成是 Show-and-tell。基于我们对已登录用户的了解,可以使用 JavaScript. 选择性地替换或注入 Web 页的部分内容,从而为用户提供正确的体验。为了进一步细分,我会进行如下操作:

创建只 具有所有用户的公共元素的 Web 页。

当用户登录时,将一些有关该用户的数据存入 cookie,比 如说登录信息。

然后,使用 JavaScript. 依据 cookie 的内容注入 HTML 段,借此填充该页面的 剩余部分。

对于 ChangingThePresent 主页而言,show-and-tell 技巧有些威力过度,因为我只 有两套链接要根据所登录的用户加以显示。因此,我选择了第二种技巧,我称之为 hide-and-seek。首先 ,显示出所有用户的公共页面元素,并通过每种数据可能 的隐藏版本显示页面的变化部分。这就是 hide 部分。然后,根据用户的角色使用 JavaScript. 在文件中找到该用户的内容并显示出来。这就是 seek 部 分。您可能会想,显示所有可能数据的版本有点威力过度,实际上,选择性地为不同的安全角色启用多种 特性时,这种方式是十分常见的。hide-and-seek 方式非常适合 ChangingThePresent 主页。要实现这种 方法,可以执行如下操作:

创建只具有所有用户的公共元素的 Web 页。

将用户按类型分区。为每个用户类型添加内容版 本。就我的具体情况而言,ChangingThePresent 主页的用户类型包括登录用户和登出用户。最初,让此 内容可见。

当用户登录时,将一些可区分用户分组的数据存入 cookie,比如说用户角色或登录 状态。

当用户访问此页时,选择性地显示用户类型的内容版本。

实现 hide and seek

对于 ChangingThePresent 主页而言,hide-and-seek 实现起来异常简单。在之前的图 1 中 ,此主页有一个部分显示的是与用户帐户相关的一些链接。这些链接可以根据用户是否登录而变化。首要 工作是构建此页的所有公共内容。我在本文并未给出具体做法。第二页需要显示出所有用户的全部动态内 容,而不管用户是否已经登录:

清单 1. 在单一视图中创建动态内容的所有版本

<div id='logged_out'> <%= link_to "login", :controller => 'members', :action => 'login' %> <br /> <%= link_to "register", :controller => 'members', :action => 'signup' %></div><div id='logged_in' style="display: none;"> <%= link_to "your profile", :controller => 'profiles', :action => 'show' %> <%= link_to "logout" , :controller => "members", :action => "logout" %></div>

您可能已经注意到 my profile 链接。起初,该链接指向用户特定的配置文件, 但这样可能会妨碍我们的主页缓存。相反,我只简单地将此链接指向了无任何用户 ID 的索引操作。然后 ,索引操作会将用户重定向到正确的配置文件页:

清单 2. 将用户重定向到正确的配置文件页

  def index    redirect_to my_profile_url  end

在清单 2,my_profile_url 是一个方法,该方法可以根据用户的类型(这可能是名人 、顾问或会员)决定正确的配置文件 URL。每个用户类型都有一个单独的配置文件页。这时,程序的功能 已经完成,您总共可以看到四个链接,logged_in 和 logged_out 各有两个链接:

login

register

your profile

logout

下一步,获取含有当前用户类型的 cookie。对于 ChangingThePresent,我在登录时创建了一个 cookie,其中含有当前的登录 ID。之后, 在登出时再销毁这个 cookie:

清单 3. 在登录和登出时创建和销毁 cookies

def login if request.post?  self.current_user = User.authenticate(params['user_login'], params['user_password'])  ...  if logged_in?   set_cookies   ...  endenddef logoutendprivatedef set_cookies cookies[:login] = current_user.login cookies[:image] = find_thumb(current_user.member_image)enddef logout cookies.delete :login cookies.delete :image ...end

在清单 3 中,logged_in? 是一个私有方法,如果当前用户已登录则返回 true。上述的 Rails 方法会在您登录时创建三个 cookie,并在登出时删除它们。这里不需要为数据费神。尚且不需用 到数据。可以这样理解:无需调用 Rails 框架,我就可以判断用户是否登录。我无需确保 cookie 到期 终止是否与站点的到期终止规定相符。在我的例子中,二者是相符的,所以我现在尽可以开始页面缓存了 。

下一步,根据用户的 cookie 选择性地隐藏和显示正确的条目。将如下的 JavaScript. 代码添 加到 public/javascripts/application.js 中:

清单 4. 支持 show and hide 登录 div 的 JavaScript. 代码

function readCookie(name) {  var nameEQ = name + "=";  var ca = document.cookie.split(';');  for(var i=0;i < ca.length;i++) {    var c = ca[i];    while (c.charAt(0)==' ') c = c.substring(1,c.length);    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);  }  return null;}function handle_cached_user() {  var login_cookie = readCookie('login');  var logged_in = document.getElementById('logged_in');  var logged_out = document.getElementById('logged_out');  if(login_cookie == null) {   logged_in.style.display = 'none';   logged_out.style.display = 'block';  } else {   logged_out.style.display = 'none';   logged_in.style.display = 'block';  }}

第一个函数从 Javascript. 中读取 cookie 值,第二个函数处理此 DOM。可以通过使用 Prototype 库 简化这段代码,但我包括了基本的 DOM 查找以便于读者理解。最后一步是在页面加载时调用 JavaScript. 函数。我向布局中添加了如下代码:

清单 5. 当页面加载时调用 JavaScript. 函数

  <script. type="text/javascript">   window.onload = function() {     handle_cached_user();      <%= render_nifty_corners_javascript. %>      <%= yield :javascript_window_onload %>   }  </script>

上述 JavaScript. 代码十分简单。在页面加载时,将加载 handle_cached_user 函数,而它会相应地显示或隐藏正确的内容。现在,我尽可以通过向控制器中添加 如下代码来启用页面缓存:

  caches_page :index

上述代码效果极佳。我还是需要 定期地从缓存中删除前页,这样我才能使该页期满终止。为此,我只需简单地定期删除 public/index.html。hide-and-seek 方式对于有几类用户的页面十分有效,但对于如图 2 中所示的用户 partial 效果却不佳。对于后者,需要综合使用 hide-and-seek 和 show-and-tell 技巧。

实现 show-and-tell

再来看看 图 2。我将使用 hide-and-seek — 根据用户是否已登录 — 选择 partial 的正确版本,然后使用 show-and-tell 技巧根据我之前在清单 3 的行 4 和行 5 中所写 的 cookies 的内容填充页面的动态部分。请记住,对于 show-and-tell,我特别更改了页面的元素以符 合单个用户的情况。

首先,完成在这两个 partial (即登出用户和登录用户)上呈现的静态内容 。我假设用户已经登出,所以我会通过附加 display: none 风格隐藏 logged_in div。之后,如果必要 ,我就可以用 JavaScript. 显示或隐藏它们。请注意,我使用了相同的两个名称:logged_in 和 logged_out,来识别每个 div,这样便无需对我为这个主页所编写的 JavaScript. 进行修改:

清 单 6. 呈现登录和登出这两个 partial

<div class="boxRight sideColumnColor">  <div id='logged_in'>    <%= render :partial => 'common/logged_in' style="display: none; %>  </div>  <div id='logged_out'>    <%= render :partial => 'common/logged_out' %>  </div></div>

接下来,完成 logged_in partial 的内容。注意,每个包含动态内容的 HTML 组 件都有一个 ID,从而我可以使用 JavaScript. 找到它并随后将其替换:

清单 7. 显示 logged_in partial

<div id='logged_in' style="display: none;"> <%= link_to %(<span class="mainBodyDark">Hi, </span>) +    %(<span class="textLarge mainBodyDark"><b id='bold_link'>) + "my_login" +    %(</b></span>), {:controller => 'profiles', :action => 'show', :id => 'my_login'},{:id => 'profile_link'} %> <br/> <div id='picture_and_link'>   <a href="http://member/my_login" id='link_for_member_thumbnail'>     <img id='member_thumbnail'        alt="Def_member_thumbnail"        src="/images/default/def_member_thumbnail.gif" /></a> </div> <div id="not_mine">Not my_login?</div> <br/> <%= image_button "logout", :controller => "members", :action => "logout" %>

如果对 Rails 有足够的了解,您可能会注意到其中的几个定 制帮助程序函数。从中可以看到四处很明显的动态内容,我需要使用 JavaScript. 为每个加载的页面替换 这些内容:三处登录,一处会员图像。此处的 JavaScript. 代码对 handle_cached_user 函数作了一处修 改,并且还含有一个为动态用户处理页面更新的方法。针对本文的具体情况,我稍微对这段代码做了少许 简化。可以将如下函数添加到 application.js 文件中:

清单 8. 替换用户 partial 的元素

function handle_user_partial() {  var login_cookie = readCookie('login');  var image_cookie = readCookie('image');  var profileLink = document.getElementById('profile_link');  profileLink.href = '/member/' + login_cookie;  document.getElementById('bold_link').firstChild.nodeValue=login_cookie;  document.getElementById('not_mine').firstChild.nodeValue="Not " + login_cookie + "?";  document.getElementById('link_for_member_thumbnail').href="/member/" + login_cookie;  document.getElementById('member_thumbnail').src=image_cookie.replace(/%2[Ff]/g,"/");  document.getElementById('member_thumbnail').alt=login_cookie;}

在清单 8 中,这个 JavaScript. 函数首先读取此 cookies 并获取 DOM 树的一部分:即到当 前的用户配置文件的链接,称为 profile_link。然后是 handle_user_partial 函数:

将登录用户的名称(存储在 login_cookie 内)替换成 my_login 以为用户配置文件页创建正确的 URL。

将登录用户名插入到 DOM 元素中,此元素使用粗体文本表示登录用户。

将简单的句子 “Not login?” 插入到 DOM 元素中,这个元素包含 login partial 中的 logout 标 题。

找到包含会员图像的 dom 元素,将一般图像的 URL 替换成会员图像的 URL,会员图像保存在 image_cookie 中。

此外,还要将此图像的 alt 标记替换成 login 名称,以防图像不出现。

在 DOM 中导航时,会发现有时需要直接转到 DOM 元素,而有时又需要转到该元素的特定子元素,比 如在处理文本的时候。我就使用了 firstChild 函数根据需要寻找 DOM 元素的第一个子元素。由于语法 更为友好,所以 Prototype 库使处理特定的 DOM 元素较为容易一些,但这超出了本文的讨论范围。

我已经创建好了所有的 cookies,最后一步就是从 handle_cached_user 函数调用 JavaScript。请记 住,该函数在 public/javascripts/application.js中:

清单 9. 将 handle_user_partial 函数添加到 handle_cached_user

function handle_cached_user() {var login_cookie = readCookie('login');    var logged_in = document.getElementById('logged_in');    var logged_out = document.getElementById('logged_out');    if(login_cookie == null) {      logged_in.style.display = 'none';      logged_out.style.display = 'block';    } else {  handle_user_partial();      logged_out.style.display = 'none';      logged_in.style.display = 'block';    }}

请注意,else 条件中的 handle_cached_user 函数下面还有额外两行代码。这两行代码可以在使 logged_in DOM 元素可见之前进行适当的替代。剩下所需做的就是使用本篇文章和 上个月 的那篇文章中 所介绍的页面缓存指令来缓存整个页。

结束语

本篇文章中介绍的这种高级技巧为我们打开了许多方便之门。在 ChangingThePresent.org 上,我们 估计使用非常简单的基于时间的清除器能够缓存超过 75% 的页面。通过使用稍微有些复杂的清除技巧, 我们就能缓存超过 90% 的页面,而且还可能更多。如果您想试图影响我们的图像缓存计划,那么您只能 触及应用服务器 1% 到 3% 的 Web 请求。

但同时,我们也应该看到不利的一面。我向此系统添加了明显的复杂性。我必须维护更加复杂的 HTML 代码,并确保 HTML 和 JavaScript. 能够保持同步。但好的一面是在需要获得更好的性能时,我就能够使 用最为简单和有效的缓存技术。您也可以尝试使用这种技巧 — 访问 ChangingThePresent.org 并加载主 页。接下来,加载每个顶端的菜单。您会发现我们会页面缓存六个顶端菜单中的四个。创建一个帐号并重 载每一个菜单。您能猜到哪个页面被缓存了么?在下一篇文章中,在继续深入真实世界中的 Rails 的同 时,我将带您探究能增进 ActiveRecord 性能的一些技巧。

一直觉得人应该去旅行,在年轻的时候,趁着有脾气装潇洒,

真实世界中的Rails,第2部分:高级页面缓存

相关文章:

你感兴趣的文章:

标签云: