Agile Web Development with Rails 翻译(十五)
整理购物车
在我们完成这段工作并向客户展示之前,让我们整理一下购物车显示页面。而不简单地倾销这个产品,让我们添加一些格式化。同时,我们可以添加些持续购物的连接,以便我们不必只能按下Back按钮。在我们添加连接时,让我们在付款后再添加个空连接给购物车。
2006年4月17日更新
我们新的display_cart.rhtml文件看起来像这样:
<div id="cartmenu">
<ul>
<li><%= link_to 'Continue shopping', :action => "index" %></li>
<li><%= link_to 'Empty cart', :action => "empty_cart" %></li>
<li><%= link_to 'Checkout', :action => "checkout" %></li>
</ul>
</div>
<table cellpadding="10" cellspacing="0">
<tr class="carttitle">
<td rowspan="2">Qty</td>
<td rowspan="2">Description</td>
<td colspan="2">Price</td>
</tr>
<tr class="carttitle">
<td>Each</td>
<td>Total</td>
</tr>
<%
for item in @items
product = item.product
-%>
<tr>
<td><%= item.quantity %></td>
<td><%= h(product.title) %></td>
<td align="right"><%= item.unit_price %></td>
<td align="right"><%= item.unit_price * item.quantity %></td>
</tr>
<% end %>
<tr>
<td colspan="3" align="right"><strong>Total:</strong></td>
<td id="totalcell"><%= @cart.total_price %></td>
</tr>
</table>
在用了些CSS魔术后,我们购物车看起来像图8.3。
很高兴我们做了这些工作,我们找来客户并显示给他我们上午的工作。他很高兴看到站点被组装在一起。可是当她读了一篇关于进行中的电子化商业站点每天被攻击并危及安全的商业新闻后,她很忧虑。她读到的一种攻击方法是向web应用送去带有不正确参数的请求,以期暴露缺陷和安全漏洞。他注意到添加项目给购物车的连接看起来像这样store/add_to_car/nnn,而nnn是我们内部的产品id。像是恶意地,他向浏览器手工输入了这个请求,并给它产品”wibble”的id号。当他看到图8.4的显示时没说什么。所以下一次,要花费些时间让应用程序更有弹性。
8.4 循环 C2: 处理错误
看看图8.4的显示,它显示出是我们的应用程序在store “控制器”的第八行抛出了一异常。就是这行:
product = Product.find(params[:id])
如果产品没有找到,“活动记录”抛出RecordNotFound异常,我们需要处理。问题是我们该如何做?
我们可以默默地忽略它。从安全角度看,移除它或许是最好的,而不要给潜在的攻击者任何信息。但是,它也意味着我们不应该有生成错误产品id的代码,并且我们应用程序不会应答这种错误—没有人知道这是个错误。
相反,当一个异常被抛出时,我们接受三个动作。首先我们将使用Rails的日志功能将其记录到日志上(描述在186页)。其次,我们将输出个短消息告诉用户。最后,我们将重新显示分类目录页给用户以便它能继续浏览我们站点。
The Flash!
就像你猜到的,Rails有个方便的方式来处理错误和错误报告。它定义了一结构叫flash。一个flash是个“桶”(实际上更像个哈希表),在它那里你可以存储些你处理一个请求的东西。Flash内容在被自动删除之前,对这个“会话”的下一次请求是有效的。典型地flash被用于收集错误消息。例如,当我们的add_to_cart()动作侦测到它被传递了一个无效的产品id时,它可以存储那个错误信息在flash内,并重定向index()动作来重新显示目录。Index动作的“视图”可以抽取错误并在分类目录页的顶部显示这个错误。flash信息可在“视图”内通过@flash实例变量来访问。
为什么我们不能只存储错误到任何原有实例变量中呢?记住重定向是由我们的应用发送给浏览器的,那将会发出一个新的到应用的请求。我们收到请求时,应用已经继续迁移,所有来自以前请求的实例变量都永远消失了。flash 数据被存储在“会话”内就是为了让在它两个请求之间有效。
通过flash数据防止此类错误,现在,我们可修改我们的add_to_cart()方法让它在接受错误产品id并报告此问题。
def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@cart.add_product(product)
redirect_to(:action => 'display_cart')
rescue
logger.error("Attempt to access invalid product #{params[:id]}")
flash[:notice] = 'Invalid product'
redirect_to(:action => 'index')
end
rescue子句可截获由Product.find()抛出的异常。在处理程序中,我们使用了Rails的logger来记录这个错误,用一个说明创建一个flash通知,然后重定向回分类目录显示页。(为什么重定向,而不直接在此处显示分类目录页呢?如果我们重定向的话,浏览器的URL尾部显示为http://.../store/index,而不是http://.../store/add_to_cart/wibble。我们以这种方式减少对应用程序显露。我们也防止用户通过按下Reload按钮再次钉住错误。)
我们可以再将客户输入的错误再来一次。这些,当我们明确地输入下面命令时
http://localhost:3000/store/add_to_cart/wibble
我们没有再浏览器内看到堆错误。相反,分类目录页被显示。如果我们看看日志文件的尾部(在log目录下的development.log文件),我们看看到我们的消息。
Parameters: {"action"=>"add_to_cart", "id"=>"wibble", "controller"=>"store"}
Product Load (0.000439) SELECT * FROM products WHERE id = 'wibble' LIMIT 1
Attempt to access invalid product wibble
Redirected to http://localhost:3000/store/
: : :
Rendering store/index within layouts/store
Rendering layouts/store (200 OK)
Completed in 0.006420 (155 reqs/sec) | Rendering: 0.003720
(57%) | DB: 0.001650 (25%)
日志开始工作了,但flash消息并没有出现在用户的浏览器上。这是因为我们没显示它。我们将显示添加一些东西给“层”以告诉它,如果它们退出了要显示flash消息。下面rhtml代码检查通知级别的flash消息,如果需要的话并创建一个新的<div>包含它。
<% if @flash[:notice] -%>
<div id="notice"><%= @flash[:notice] %></div>
<% end -%>
那么我们把代码放哪儿呢?我们可以放在分类目录显示“模板”的顶部—在index.rhtml代码内。毕竟,我们认为那里是正确的地方。但是当我们继续开发时,我们会觉得有个标准的错误显示方式是最好的。我们已经使用Rails的“层”来得到所有store页面的一致性外观,所以让我们添加个flash处理代码给那个“层”。如果我们的客户突然决定错误在工具条内看起来会更好,那么我们可以只修改一处,我们所有存储的页都会被更新。所以我们新的store “层”代码看起来像这样。
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot", :media => "all" %>
</head>
<body>
<div id="banner">
<img src="/images/logo.png"/>
<%= @page_title || "Pragmatic Bookshelf" %>
</div>
<div id="columns">
<div id="side">
<a href="http://www....">Home</a><br />
<a href="http://www..../faq">Questions</a><br />
<a href="http://www..../news">News</a><br />
<a href="http://www..../contact">Contact</a><br />
</div>
<div id="main">
<% if @flash[:notice] -%>
<div id="notice"><%= @flash[:notice] %></div>
<% end -%>
<%= @content_for_layout %>
</div>
</div>
</body>
</html>
这次,当们手工输入无效的产品id时,我们看到位于分类目录页顶部错误报告。
当我们以这种方式查看时,怀有恶意的用户会弄乱我们应用程序,我们注意到当购物车为空时,那个display_cart()“动作”将会被直接从浏览器调用。这不是个大问题—它只是显示一个空列表和为零的合计,但我们应该比这做的更好。我们可以使用我们的flash功能做重定向时显示一个好的提示给试图使用空购物车用户。我们将修改store “控制器”内的display_cart()方法。
def display_cart
@cart = find_cart
@items = @cart.items
if @items.empty?
flash[:notice] = "Your cart is currently empty"
redirect_to(:action => 'index')
end
end
-----------------------------------------------------------------
David 说. . .
需要多少内置的错误处理程序?
add_to_cart()方法显示了Rails内错误处理的高级版本,那里特定的错误被给预了足够的关注和代码。不可能花费时间来想着捕获所有的错误。许多输入错误将引发应用程序引产异常。所以们很少这样做,我们只是把异常视为一个统一的catchall错误页。这样一个错误页可以在ApplicationController内实现,那里rescue_action_in_public(异常)方法将在异常发生但没有被低层所捕获时被调用。这个技术的更多信息在440页的第二十二章。
------------------------------------------------------------------
记得我们在71页使用@page_title(如果定义了的话)的值来设置store “层”。让我们现在使用这个功能。编辑模板display_cart.rhtml来覆写这个页标题,无论其是否被使用。这有个好特性:在模板内设置的实例变量在“层”中是有效的。
<% @page_title = "Your Pragmatic Cart" -%>
在感觉到快要结束时,我们找来客户并显示我们现在已经适当的进行了错误处理。他很满意并继续试验我们的应用程序。对我们的新购物车显示页面他注意到两点。首先,空的购物车按钮不应该连接到任何东西。其次,如果添加同样书两次给购物车,它显示合计金额为59.9而不是$59.90。这两个微小改动在下次修改时完成。