Agile Web Development with Rails 翻译(十四)
2006年4月17日更新
8.3 循环 C1: 创建个购物车
读者可能注意到了我们的分类目录列表“视图”已经包含了一个Add to Cart连接给每个产品列表。
<%= link_to 'Add to Cart',
{:action => 'add_to_cart', :id => product },
:class => 'addtocart' %><br/>
这个连接点由store内“控制器”的add_to_cart()“动作”支持,并会传递产品的id做为表单的参数。[说:id=>product,是:id=>product.id的习惯缩写。两者将产品的id传回给“控制器”。]从这里我们看到了我们“模型”内的id字段是如何的重要了。Rails通过它们的id字段标识“模型”对象(及相应的数据库的行)。如果我们传递一个id给add_to_cart(),我们会添加具有唯一标识的产品。
现在,让我们实现add_to_cart()方法。它需要为当前“会话”(如果没有则创建一个)找到购物车,并添加选择的产品到这个购物车中,然后显示此购物车中的内容。由于细节并不很麻烦,让我们只写出抽象级代码。我们将在app/controllers/store_controller.rb文件内创建一个add_to_cart()方法。它使用参数对象来从请求中获得id参数,然后找出相应产品,并使用我们先前创建的find_cart()方法来找到此“会话”内的购物车,并添加产品给此购物车。当给“控制器”添加add_to_cart()方法时要小心。因为它被作为一个“动作”来调用,它必须是public的,所以必须被添加到private的find_cart()方法的上面。
def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@cart.add_product(product)
redirect_to(:action => 'display_cart')
end
很明显,这个代码也不会运行:我们没有创建Cart类,并且我们也没对display_cart()功能的任何实现。
让我们开始创建Cart类和它的add_product()方法。因为它存储应用程序数据,它是我们“模型”的逻辑部分,所以我们将创建文件cart.rb在目录app/models内。虽然,它与数据库表没有联系,因此它不是ActiveRecord::Base的子类。
class Cart
attr_reader :items
attr_reader :total_price
def initialize
@items = []
@total_price = 0.0
end
def add_product(product)
@items << LineItem.for_product(product)
@total_price += product.price
end
end
这很直截了当。我们基于产品创建了一个新的商品项并添加它到列表中。当然,我们也没有一个方法来创建一个基于某个产品信息的商品项,所以让我们现在调整一下。我们会打开app/models/line_item.rb文件,并添加一个类方法for_product()给它。创建这类级别的方法是为了让你的代码整洁易于阅读。
class LineItem < ActiveRecord::Base
belongs_to :product
def self.for_product(product)
item = self.new
item.quantity = 1
item.product = product
item.unit_price = product.price
item
end
end
现在我们创建了一个Cart类来保持我们的商品项,并且我们在“控制器”内实现了add_to_cart()方法。并依次调用新的find_cart()方法,这个方法确保我们保持“会话”内的购物车对象。
我们还需要实现display_cart()方法和相应的“视图”。同时,我们已经写了这么多代码却没有尝试,让我们加入一些模拟数据(stub)来看看怎么样。在store “控制器”中,我们将实现一个“动作”方法来处理引入的请求。
def display_cart
@cart = find_cart
@items = @cart.items
end
在app/views/store目录内,我们会为相应的“视图”创建个display_cart.rhtml文件。
<h1>Display Cart</h1>
<p>
Your cart contains <%= @items.size %> items.
</p>
我们已准备好了所有东西,现在让我们在浏览器内看看我们的商店。导航到http://localhost:300/stroe调用我们的分类目录页。单击每个产品的Add to Cart连接。[如果你没有看到产品列表,你将需要退回到应用程序的管理一节。]我们期望看到购物车显示页面,但我们看到的却是惨不忍睹的页面。
首先,我们可能会想到我们拼错了“动作”方法的名字或者是“视图”的名字,但事实不是这样。这不是Rails的错误消息—它来自于WEBrick。想找出原因,我们需要看看WEBrick的控制台输出。进入WEBrick的运行窗口,你会看到登录和跟踪消息。跟踪指出了应用程序内错误的原因。(技术上说,这是个stack backtrace,它显示了应用程序阻塞点调用的方法链。)可以很容易地通过回卷来找出错误。在开始前,你将看到一个错误信息。
#<ActionController::SessionRestoreError: Session contained
objects where the class definition wasn't available. Remember
to require classes for all objects kept in the session. The
session has been deleted.>
当Rails试图加载来自于浏览器cookie的“会话”信息时,它会遍历一些它还不知道的类。我们必须告诉Rails有关我们Cart和LineItem类。(83页的注释解释了为什么。)在app/controllers目录内你会找到个名为application.rb的文件。这个文件用于构建应用程序入口的上下文环境。缺省情况下,它包含一个空类ApplicationController的定义。我们需要在其中添加两行来声明我们的新“模型”文件。
class ApplicationController < ActionController::Base
model :cart
model :line_item
end
现在,如果我们刷新我们的浏览器,我们应该看到“视图”显示了。(如图8.2)如果我们使用Back按钮返回分类目录显示,并添加另一个产品给购物车,你将会看到当购物车页被显示时,计数被更新了。看起来们的“会话”工作了。
现在最困难的事情我们已做完了。这确实是我们能够为我们的客户展示什么之前所花费的最久时间。 但是现在我们应该将每个东西正确地连在一起,让我们快速地实现一个简单的购物车显示,以便我们尽快得到客户的反馈。我们用下面的代码来替换原有购物车内的display_cart.rhtml文件内代码。
<h1>Display Cart</h1>
<table>
<%
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 -%>
</table>
这个“模板”显示了很多ERb特性。如果我们用-%>(注意有个减号)来结束植入的Ruby语句,ERb将抑止随后的新行。这意味着被植入的Ruby不能产生任何输出。
-------------------------------------------------------------------------------
“会话”信息,序列化,和类
Session结构存储浏览器请求间你想保持的对象。要想工作,Rails必须能接受这些对象,并存储它们在一个请求的尾部,当同一浏览器有后续请求时将它们加载回来。要在运行的应用程序外部存储对象,Rails使用了Ruby的序列化机制,它转换对象为可被稍后能重新取回的数据。当我们在一个“会话”内存储一个购物车时,我们存储了类Cart的一个对象。但是当加载回数据时,Rails并不保证加载此点上的原有Cart “模型”(因为Rails只加载它认为它需要东西)。可以使用“模型”声明来强制Rails加载先前用户“模型”类,所以当Ruby加载序列化的“会话”时,它知道它在做什么。
-------------------------------------------------------------------------------
刷新浏览器,(假设我们从分类目录中选择了一个产品)我们会看到它显示。
1 Pragmatic Project Automation 29.95 29.95
单击Back按钮,添加另一个产品。
1 Pragmatic Project Automation 29.95 29.95
1 Pragmatic Version Control 29.95 29.95
看起来不错,返回并再选择原有产品一次。
1 Pragmatic Project Automation 29.95 29.95
1 Pragmatic Version Control 29.95 29.95
1 Pragmatic Project Automation 29.95 29.95
这看起来可不好,尽管购物车逻辑上是正确的,但它与我们想的不一样。相反,我们或许应该将两者自动地合并成一个数量为2的单独的行条目。
幸运地,这改起来很容易,通过给Cart “模型”添加add_product()方法。当添加一个新产品时,我们会看到产品已在那个购物车内了。如果是这样话,我们将只增加它的数量,而不添加个新的商品项。记住购物车不是个数据库对象—它只是Ruby代码。
def add_product(product)
item = @items.find {|i| i.product_id == product.id}
if item
item.quantity += 1
else
item = LineItem.for_product(product)
@items << item
end
@total_price += product.price
end
我们现在面对的问题是,我们在购物车内已经有个一个带有重复产品的“会话”,这个“会话”与我们浏览器内存储的一个cookie相关联。它不会自动离去,除非我们删除那个cookie。[如果你想的话,你可这样做。你可以删除cookie文件。] 幸运的是,当我们想测试我们的代码时有除了点击浏览器按钮以外的方法。Rails的方法是写测试。 但是这是个很大的题目,我们把它单独地放在了一章,132页的第十二章。