打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

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

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

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

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


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

真实世界中的 Rails 系列文章中,在国际上享有盛誉的作家兼讲演人 Bruce Tate 将带您深入到真实世界中的 Rails 开发。作为 WellGood LLC 公司的 CTO,他主要负责设计、构建和维护慈善性质的门户网站 ChangingThePresent.org,在这里,癌症研究员可以贡献一小时的研究时间,您可以为保护热带雨林做出自己的一份贡献,您也可以资助白内障手术,让失明的人得以重见光明。许多用户都通过 ChangingThePresent 找到了数千家非赢利组织,此网站的影响和规模还在不断扩大。

现在,您可以找到数十篇帮助您构建简单 Rails 应用程序的文章。本系列无意介绍构建简单 blog 的基础知识,而是会带您深入到每个 Rails 网站都必须要面对和解决的实际问题。通过本系列文章,您将学习如何优化 Rails 以及如何让站点更加稳定。而且,您还将了解如何通过添加插件来解决 Rails 的一些基本限制。读完本系列的全部文章之后,您将对如何实际运行 Rails 站点有一个初步的了解。

图 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_inlogged_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
                        ...
                        end
                        end
                        def logout
                        end
                        private
                        def set_cookies
                        cookies[:login] = current_user.login
                        cookies[:image] = find_thumb(current_user.member_image)
                        end
                        def 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_inlogged_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 性能的一些技巧。



参考资料

学习

获得产品和技术


关于作者

Bruce Tate 居住在德克萨斯州的首府奥斯汀,他是一位父亲,同时也是山地车手和皮艇手。他是 WellGood LLC 的 CTO 及 ChangingThePresent.org 的首席架构师,他还是九本畅销书的作者,包括 Beyond Java、From Java to Ruby 和 Ruby on Rails: Up and Running。他在 IBM 工作了 13 年,现在是 RapidRed 顾问的创始人,在那里他潜心研究基于 Ruby 的轻量级开发策略和架构以及 Ruby on Rails 框架。他现在还与很多 Rails 开发人员合作,构建和维护一个慈善性质的门户网站 ChangingThePresent.org。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
第5章 API应用案例(上)
你应该知道的9款流行 Web 框架及其优缺点
为有抱负的开发者推荐的最佳 10 门编程语言
我为何要弃 Java、JavaScript、Ruby 于不顾,而去寻找新的编程语言?
JavaScript history 对象
用 Selenium 自动化验收测试
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服