第七章
分类显示
ruby script/generate controller store index
class StoreController < ApplicationController
def index
@products = Product.find_products_for_sale
end
end
class Product < ActiveRecord::Base
def self.find_products_for_sale
find(:all, :order => "title" )
end
depot_d/app/views/store/index.html.erb
<h1>Your Pragmatic Catalog</h1>
<% for product in @products -%>
<div class="entry">
<%= image_tag(product.image_url) %>
<h3><%=h product.title %></h3>
<%= product.description %>
<div class="price-line">
<span class="price"><%= product.price %></span>
</div>
</div>
<% end %>
h(string)方法对货品名称中出现的HTML 元素进行转义处理,但并没有对货品
描述做同样的处理。这样一来,我们就可以在其中加入HTML 样式,让描述信息更加吸引顾客
添加页面布局
在Rails 中,定义和使用布局的方式有好多种,我们现在就使用最简单的一种。如果我们在app/views/layouts目录中创建了一个与某个控制器同名的模板文件,那么该控制器所渲染的视图在默认状态下会使用此布局模板。所以,我们现在就来创建这样一个模板。我们的控制器名叫store ,所以这个布局模板的名字就应该是store.html.erb 。
<!DOCTYPE html PUBLIC "//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd" >
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
</head>
<body id="store">
<div id="banner">
<%= image_tag("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">
<%= yield :layout %>
</div>
</div>
</body>
</html>
当我们调用yield 方法并传入:layout时, Rails 会自动在这里插入页面的内容
用辅助方法格式化价格
<span class="price" ><%= product.price %></span>
修改为
<span class="price"><%= number_to_currency(product.price) %></span>
链接到购物车
link_to()辅助方法生成的是<a href=...> 这样一个HTML 标签;当你点击链接时,浏览器会向服务器发起一个HTTP GET 请求。而HTTP GET 请求不应该用来改变服务器上任何东西的状态——只应该用于获取信息。
:method=>:post 可以解决这个问题。此外button_to()方法也可以从视图链回应用程序,但它生成的是一个HTML 表单,其中只包含一个按钮。当用户点击这个按钮时,浏览器会向服务器发起一个HTTP POST 请求。
<div class="price-line">
<span class="price"><%= number_to_currency(product.price) %></span>
<%= button_to "Add to Cart" %>
</div>
第八章 创建购物车
实现session 的底层机制有很多种。有时应用程序会将session 信息编码在每个页面的表单数据中,有时会在每个URL 的末尾加上经过编码的session 标识串( 也就是所谓的“ URL 重写” ) ,还有时会用cookie 来实现session。Rails 就默认使用了基于cookie 的做法。
Rails 为这些底层细节提供了一个简单的抽象接口,让开发者不必操心协议、cookie 之类的事情。在控制器中, Rails 维护了一个特殊的、类似于hash 的集合,名为session。在处理请求的过程中,如果你将一个名/ 值对保存在这个hash 中,那么在处理同一个浏览器发出的后续请求时都可以获取到该名/ 值对。
一种方法是将session 信息保存在服务器端的文件中。如果你只运行了一个Rails 服务器,不会有什么问题。但假如你的在线商店非常热门,以至于一台服务器不能应付,必须在多台服务器上同时运行,问题就来了。对于某个特定的用户,她的第一个请求可能被引导到一台后端服务器上.第二个请求则被引导到另一台服务器,而两台服务器又不能共享彼此储存的session数据,这时用户就会迷惑地发现:购物车里的货品一会出现,一会又消失了。
所以,最好是把session信息保存在应用程序之外的某个地方,这样多个应用程序进程就可以共享这些数据。而且,如果这个外部存储介质是持久化的,我们甚至还可以中途重启服务器,而不会丢失任何session信息。
把session 放在数据库中
创建一个数据迁移任务来定义session数据表
rake db:sessions:create
把数据库表创建出来
rake db:migrate
在config 目录下的某个文件中去配置environment.rb 文件(2.3.5为config/initializers/session_store.rb)
# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with 'rake db:sessions:create')
config.action_controller.session_store = :active_record_store
如果使用cookie 以外的方式, 你还需要做一件事: 删掉app/controlle/application.rb46文件中secret 一行中的的“ #”:
(2.3.5版本下用scaffold生成product时application_controller下没有:secret项,可从别处拷贝)
protect_from_forgery :secret => '8fc080370e56e929a2d5afca5540a0f7'
停止script/server ,然后再次运行
购物车和session
在store控制器中创建一个find_cart()方法,
private
def find_cart
unless session[:cart] # if there's no cart in the session
session[:cart] = Cart.new # add a new one
end
session[:cart] # return existing or new cart
end
或
session[:cart] ||= Cart.new
创建购物车
创建cart.rb文件,把它放在app/models 目录
class Cart
attr_reader :items
def initialize
@items = []
end
def add_product(product)
@items << product
end
end
depot_f/app/views/store/index.html.erb
<%= button_to "Add to Cart" , :action => 'add_to_cart', :id => product %>
depot_f/app/controllers/store_controller.rb
def add_to_cart
@cart = find_cart
product = Product.find(params[:id])
@cart.add_product(product)
end
depot_f/app/views/store/add_to_cart.html.erb
<h2>Your Pragmatic Cart</h2>
<ul>
<% for item in @cart.items %>
<li><%=h item.title %></li>
<% end %>
</ul>
创建更聪明的购物车
给购物车中的货品加上“数量”信息。于是我们决定新建一个模型类CartItem ,其中引用一种货品,并包含数量信息。
class CartItem
attr_reader :product, :quantity
def initialize(product)
@product = product
@quantity = 1
end
def increment_quantity
@quantity += 1
end
def title
@product.title
end
def price
@product.price * @quantity
end
end
在Cart 类的add_product()方法中使用这个新建的模型类了
def add_product(product)
current_item = @items.find {|item| item.product == product}
if current_item
current_item.increment_quantity
else
@items << CartItem.new(product)
end
end
对add_to_cart视图做了简单的修改,将“数量”这项新的信息显示出来。
<li><%= item.quantity %> × <%=h item.title %></li>
点击Add to Cart按钮
NoMethodError in StoreController#add_to_cart
undefined method `product' for #<Product:0x3880b00>
session 中的购物车对象还是旧版本的:它直接把货品放进@items 数组。所以,当Rails 从session 中取出这个购物车之后,里面装的都是Product 对象,而不是CartItem 对象。
最简单的办法就是删除旧的session,把原来那个购物车留下的所有印记都抹掉。由于我们使用数据库来保存session 数据,只要一个Rake 命令就可以轻松地清空sessions 表。
rake db:sessions:clear
假设Depot 应用已经发布了一个版本,其中使用了旧的Cart 对象。现在有成千上万的顾客正在忙碌地购物,而我们又决定要发布新的、更好用的购物车。我们把代码投入正式运行,突然间所有正在享受购物乐趣的顾客们在往购物车中添加货品时就会遇到错误。而我们唯一的解决办法是删除session数据,也就是说所有顾客的购物车都会被清空。
这个例子告诉我们,“把应用层面的对象放在session中”通常不是个好主意:如果这样做的话,一旦我们把修改之后的应用程序投入实际运行,就必须清空所有现存的session数据。
所以,推荐的做法是只在session 中保存尽可能简单的东西:字符串、数字,等等。应用层面的对象应该放在数据库里,然后将它们的主键放入session,需要时根据session中的主键来查找对象。
一种常见的攻击方式就是把错误的参数扔给web 应用,借此暴露出程序的bug 和安全漏洞。链接类似于store/add_to_cart/nnn 这样,其中nnn 就是货品在系统内部的id,把货品id 改成了“ wibble”。应用程序出错了,错误页面暴露了太多关于应用程序的内部信息,而且看上去也很不专业。
处理错误
当异常发生时,我们通常会做三件事:首先,利用Rails 的日志工具将这一事实记入内部日志文件;其次,向用户输出一条简短的信息;最后,重新显示分类列表页面,以便用户可以继续使用我们的站点。
Flash
对于错误处理和报告, Rails 也有简便的途径。Rails 定义了一个名叫flash 的结构——所谓flash 其实就是一个篮子( 与Hash 非常类似) ,你可以在处理请求的过程中把任何东西放进去。在同一个session的下一个请求中,你可以使用flash 的内容,然后这些内容就会被自动删除。一般来说, flash 都是用于收集错误信息的。譬如说,当add_to_cart()action 发现传入的货品id 不合法时,它就可以将错误信息保存在flash 中,并重定向到index() action ,以便重新显示分类列表页面。index() action 的视图可以将flash 中的错误信息提取出来,在列表页面顶端显示。在视图中,使用flash 方法即可访问到flash 中的信息。
为什么不把错误信息直接保存在随便一个普通的实例变量中呢?别忘了,重定向指令是由应用程序发送给浏览器的,后者收到该指令后再向应用程序发送一个新的请求。当应用程序收到后一个请求时,前一个请求中保存的实例变量都已经消失无踪了。flash 数据是保存在session中的,这样才能够在多个请求之间传递。
def add_to_cart
@cart = find_cart
product = Product.find(params[:id])
@cart.add_product(product)
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}" )
flash[:notice] = "Invalid product"
redirect_to :action => 'index'
end
用Rails 的日志记录器记下这个错误。每个控制器都可以通过logger 属性访问日志记录器,在这里我们用它来记录错误信息,因此使用的日志级别是error 。
创建一条flash 提示信息,在其中解释出错的原因。和session一样, flash 使用起来就像是在使用一个hash 。在这里,我们用:notice作为存放提示信息的键。
使用redirect_to()方法将浏览器重定向到货品列表显示页面。该方法可以接受很多参数( 和前面在模板中用过的link_to()方法类似) ,在这里,它会要求浏览器立即去请求当前控制器的index action 。为什么要重定向,而不是直接渲染货品列表页面昵? 因为如果使用重定向, 用户在浏览器上看到的URL 地址就是http://.../store/index ,而不是http://.../store/add_to_cart/wibble。这样我们就避免了暴露太多应用程序的内部信息,并且也不会因为用户点击“刷新”按钮而再次引发错误。
日志的确生效了,但flash 信息还没有在浏览器上出现,这是因为我们没有显示它。我们需要在布局上添加一些东西,以便显示flash 信息——如果有的话。下列html.erb 代码会检查notice 级别的flash 信息,如果有这样的信息存在,便新建一个<div> 来容纳它。
depot_h/app/views/layouts/store.html.erb
<% if flash[:notice] -%>
<div id="notice"><%= flash[:notice] %></div>
<% end -%>
结束购物车
在购物车中加上一个链接,同时在store 控制器中实现empty_cart() 方法。
depot_h/app/views/store/add_to_cart.html.erb
<%= button_to 'Empty cart', :action => 'empty_cart' %>
depot_h/app/controllers/store_controller.rb
def empty_cart
session[:cart] = nil
flash[:notice] = "Your cart is currently empty"
redirect_to :action => 'index'
end
现在store控制器中有两处代码做着同样的事情:在flash 中放入一段提示信息,然后重定向到首页。看起来, 应该把这段相同的代码抽取到一个方法中。于是, 我们实现了redirect_to_index()方法,并让add_to_cart()和empty_cart() 方法都使用它。
def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@cart.add_product(product)
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}" )
redirect_to_index("Invalid product" )
end
def redirect_to_index(msg)
flash[:notice] = msg
redirect_to :action => 'index'
end
depot_i/app/models/cart.rb
def total_price
@items.sum { |item| item.price }
end