Rails开发:购物车(3)

第9章  Ajax初体验

 

在过去的日子里( 大概2005 年以前) ,浏览器一律被当作“哑设备”对待。当编写基于浏览器的应用程序时,你只管把内容送给浏览器,然后忘记这次会话。另一方面,用户会填一些表单,或是点击超链接,应用程序就会收到一个请求;随后,应用程序会渲染一个完整的页面给用户。然后,整个乏味的过程又从头开始。这也正是Depot 应用到目前为止的工作方式。


但测览器并不真的是哑巴( 谁曾想到? ) ,它们也可以运行代码。几乎所有浏览器都可以运行JavaScript(并且大部分支持Adobe 的Flash) 。然后人们又想到,浏览器中的JavaScript 可以在背后与服务器交流,并更新用户看到的页面。Jesse James Garrett把这种交互方式命名为Ajax—— 这个词曾经是“异步JavaScript 和XML”(Asynchronous Java Script and XML)的缩写,但它现在唯一真正的意思就是“让浏览器别那么蠢”

 

那么,就来给购物车加上Ajax 吧:我们不想要一个单独的购物车页面,只要把当前的购物车显示在分类页面的边框里就行了。然后我们会加上一点Ajax 的魔法,只更新边框里的购物车,而不必重新显示整个页面。


要使用Ajax ,最好是先做出一个非Ajax 版本的应用程序,然后再逐步引入Ajax 的功能。这也正是我们将在这里做的事。首先,我们把购物车从一个独立的页面移到边框里。

 

迁移购物车

目前我们的购物车是由add_to_cart这个action 及其对应的.html.erb模板来渲染的。我们要做的第一件事,就是将“渲染购物车”的逻辑放到负责显示分类页面的布局模扳中。这很容易,使用局部模扳就行了。

局部模板

几乎每种编程语言都允许你定义方法(method) 。一个方法实际上就是一段代码,并拥有一个名字;根据名字调用方法,对应的这段代码就会运行。当然,在调用方法时可以传入参数,这样你只须编写一段代码,就可以在很多种不同的场景中使用它。

 

可以把Rails 的局部模板看作一种视图层面的“方法”。一个局部模板就是一段视图代码,被保存在一个独立的文件中。你可以从另一个模板或控制器调用( 渲染) 一个局部模板,然后这个局部模板会渲染它自己,并将渲染的结果返回给调用者。而且和方法一样,也可以向局部模板传递参数,这样同一个局部模板就可以渲染出不同的结果

 

首先来看看如何显示购物车。

 

depot_i/app/views/store/add_to_cart.html.erb

 

<div class="cart-title">Your Cart</div>
<table>
<% for cart_item in @cart.items %>
 <tr>
  <td><%= cart_item.quantity %>&times;</td>
  <td><%= h(cart_item.title) %></td>
  <td class="item-price"><%= number_to_currency(cart_item.price) %></td>
 </tr>
<% end %>
  <tr class="total-line">
  <td colspan="2">Total</td>
  <td class="total-cell"><%= number_to_currency(@cart.total_price) %></td>
 </tr>
</table>
<%= button_to "Empty cart" , :action => :empty_cart -%>
<%= button_to "Back" , :action => :index %>

 

这个模板创建了一个多行的表格,每一行用于显示购物车中的一个项目。每当你发现自己要做这样的循环操作时,就应该停下来问问自己:“模板里的逻辑会不会太多了?”看起来,我们可以把循环逻辑抽取到局部模板中

 

Rails提供了一项很有用的功能:你可以将一个集合传递给负责渲染局部模板的方法该方法就会自动地多次渲染局部模板——每次都传入集合中的一个元素作为参数。

<div class="cart-title">Your Cart</div>
<table>
 <%= render(:partial => "cart_item" , :collection => @cart.items) %> #cart_item in @cart.items
 <tr class="total-line">
  <td colspan="2">Total</td>
  <td class="total-cell"><%= number_to_currency(@cart.total_price) %></td>
 </tr>
</table>

render() 方法接收两个参数,分别是局部模板的名字和一组对象的集合。局部模板本身实际上就是另一个模板文件( 默认情况下与调用它的模板文件位于同一个目录下) 。不过,为了从名字上将局部模板和普通模板区分开来, Rails 认为局部模板的名字都以下画线开头,在查找局部模板时也会在传入的名字前面加上下画线。也就是说,我们的局部模板应该在_cart_item.html.erb文件中定义,这个文件则位于app/views/store目录下。

 

depot_j/app/views/store/_cart_item.html.erb
<tr>
<td><%= cart_item.quantity %>&times;</td>
<td><%=h cart_item.title %></td>
<td class="item-price"><%= number_to_currency(cart_item.price) %></td>
</tr>

 

在局部模板内部,我们通过cart_item这个变量来引用当前的购物车项目,因为主模板在调用render 方法时会使用这个变量——当循环渲染局部模板时,这个变量中存储的始终是“当前的购物车项目”。简而言之,局部模板的名字是“ cart_item”所以在局部模板内部就会有一个名叫cart_item的变量

 

我们已经对“显示购物车”的代码做了整理,但还没有将它搬到边框里。要实现这次搬迁,就需要将这部分逻辑放到布局模板中。如果我们编写出一个用于显示购物车的局部模板,那么只要在边框里嵌入如下调用就行了:
render(:partial => "cart" )

 

但局部模板怎么知道到哪里去获得购物车对象呢?一种可行的做法是利用隐含的共识:在布局模板中,我们可以访问控制器提供的@cart 这个实例变量,在局部模板中同样可以。但这看上去就像是调用一个方法、并通过全局变量传递值给它——这是有效的,但代码很丑陋,并且增加了耦合程度( 因此也会让你的程序变得脆弱而难以维护) 。

 

<%= render(:partial => "cart" , :object => @cart) %> # cart = @cart
这样,在_cart.html.erb 模板中就可以通过cart 变量来访问购物车。

 

注:这一切都是因为render的第二个参数,直接可以在局部模板中用同名模板接收。

 

depot_j/app/views/store/_cart.html.erb(原来的add_to_cart.html.erb)


<div class="cart-title">Your Cart</div>
<table>
<%= render(:partial => "cart_item" , :collection => cart.items) %>
<tr class="total-line">
<td colspan="2">Total</td>
<td class="total-cell"><%= number_to_currency(cart.total_price) %></td>
</tr>
</table>
<%= button_to "Empty cart" , :action => :empty_cart %>

 

修改store 布局模板,在边框中加上新建的局部模板。

depot_j/app/views/layouts/store.html.erb

 

  <div id="columns">
   <div id="side">
    <div id="cart">
     <%= render(:partial => "cart" , :object => @cart) %>
    </div>

 

现在我们必须对StoreController稍加修改,因为在访问index 这个action 时就会调用布局模板,但这个action 并没有对@cart 变量设值。

 

depot_j/app/controllers/store_controller.rb


def index
@products = Product.find_products_for_sale
@cart = find_cart
end

 

改变流程

购物车已经在边框中显示出来了,随后我们就可以改变Add to Cart按钮的工作方式:它无须显示一个单独的购物车页面,只要刷新首页就行了。这个改变也很简单:在add_to_cart 这个action 方法的最后,直接把浏览器重定向到首页。

def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@cart.add_product(product)
redirect_to_index


def redirect_to_index(msg = nil)
  flash[:notice] = msg if msg
  redirect_to :action => ‘index’
end

 

于是,在我们的在线商店中,购物车已经显示在页面的边框上了。点击按钮将货品添加到购物车之后,页面会重新显示更新后的购物车。可是,如果货品列表页面很大,重新显示这个页面可能需要花一点时间,还会占用带宽和服务器资源。还好, Ajax 可以让情况得到改善。

创建基于Ajax的购物车

我们希望让Add to Cart按钮在后台调用服务器端的add_to_card方法,随后服务器把关于购物车的HTML 发回浏览器,我们只要把服务器更新的HTML 片段替换到边框里就行了

要实现这种效果,通常的做法是首先编写在浏览器中运行的JavaScript 代码,然后编写服务器端代码与JavaScript 交互( 可能是通过JSON 之类的技术) 。好消息是,只要使用Rails ,这些东西都将被隐藏起来:我们使用Ruby( 再借助一些Rails 辅助方法) 就可以完成所有功能。

 

先从最基本的开始:修改货品列表页,让它向服务器端应用程序发起Ajax 请求;应用程序则应答一段HTML 代码,其中展示了最新的购物车。

 

在索引页上,目前我们是用button_to()来创建“ Add to Cart” 链接的。当用户点击提交按钮时,就会生成一个POST 请求。我们不希望这样,而是希望它发送一个Ajax 请求。可以使用form_remote_tag这个Rails 辅助方法, 代表它会生成HTML表单,“ remote” 则说明它会发起Ajax 远程调用。

depot_l/app/views/store/index.html.erb
<% form_remote_tag :url => { :action => 'add_to_cart', :id => product } do %>
    <%= submit_tag "Add to Cart" %>
<% end %>

 

我们还需要对应用程序做些调整,让它把Rails 需要用到的JavaScript 库发送到用户的浏览器上

 

在第24 章“ Web 2.0”(第521 页) 中,还会详细讨论这个话题

 

现在, 我们只需在store 布局的<head> 部分里调用javascript_include_tag 方法即可。

 

depot_l/app/views/layouts/store.html.erb
<html>
<head>
<title>Pragprog Books Online Store</title>
<%= stylesheet_link_tag "depot" , :media => "all" %>
<%= javascript_include_tag :defaults %>
</head>

 

到目前为止,浏览器已经能够向我们的应用程序发送Ajax 请求,下一步就是让应用程序做出应答。我们打算创建一段HTML 代码来代表购物车,然后让浏览器把这段HTML 插入当前页面的DOM,替换掉当前显示的购物车。为此,我们要做的第一个修改就是不再让add_to_cart重定向到首页。

 

def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@cart.add_product(product)
→ respond_to do |format|
→ format.js
→ end

rescue ActiveRecord::RecordNotFound

 

respond_to() 方法并告诉它我们要响应的是.js 格式文件。这条语句乍看起来有些奇怪,其实就是一个使用代码块作为参数的方法调用

 

修改的结果是,Rails 就会查照add_to_cart这个模板来执行渲染。已经被删掉的add_to_cart.html.erb(因为重定向语句被删掉了)

 

Rails 支持RJS 模板的概念——“ JS” 指的是JavaScript 。.js.rjs 模板可以将JavaScript 发送到浏览器,而你需要写的只是服务器端的Ruby 代码。下面我们就来编写第一个.rjs 模板: add_to_cart.js.rjs ,它和别的模板一样,也位于app/views/store目录下。

depot_l/app/views/store/add_to_cart.js.rjs
page.replace_html("cart" , :partial => "cart" , :object => @cart)

 

page 这个变量是JavaScript 生成器的实例——这是Rails提供的一个类,它知道如何在服务器端创建JavaScript ,并使其在浏览器上运行。 我们希望它找到当前页面上id 为cart 的元素,然后将其中的内容替换成……某些东西。

 

 排疑解难

 虽然Rails 极大地简化了Ajax ,却没办法让它变得易如反掌。而且,由于涉及到几种技术的松散整合,一旦Ajax 出现故障,可能会很难跟踪调试。这也是应该始终小步前进、逐渐增加Ajax 功能的原因。

 

删除旧的add_to_cart.html.erb 文件

记得用javascript_include_tag 将必要的JavaScript 库包含到store 布局中

浏览器是否有什么特殊的设置,强迫它每次都重新加载整个页面

是否收到了任何错误报告,察看logs 目录中的development.log文件

 

 高亮显示变化

 javascript_include_tag 辅助方法会把几个JavaScript 库加载到浏览器中,其中之一的effects.js 可以给页面装饰以各种有趣的可视化效果。在这些可视化效果中就包括如今声名显赫的“黄渐变技巧” (Yellow fade Technique)——这是一种高亮显示页面元素的技巧,通常会首先把背景变成黄色,然后再渐变到白色。

depot_m/app/models/cart.rb
 def add_product(product)
  current_item = @items.find {|item| item.product == product}
  if current_item
   current_item.increment_quantity
  else
   current_item = CartItem.new(product)
   @items << current_item
  end
  
  current_item
 end

 

depot_m/app/controllers/store_controller.rb
  def add_to_cart
    @cart = find_cart
    product = Product.find(params[:id])
    @current_item = @cart.add_product(product)

 

depot_m/app/views/store/_cart_item.html.erb
 <% if cart_item == @current_item %>
  <tr id="current_item">
 <% else %>
  <tr>
 <% end %>

<td><%= cart_item.quantity %>&times;</td>
<td><%=h cart_item.title %></td>
<td class="item-price"><%= number_to_currency(cart_item.price) %></td>
</tr>

 

depot_m/app/views/store/add_to_cart.js.rjs
page.replace_html("cart" , :partial => "cart" , :object => @cart)
page[:current_item].visual_effect :highlight,
                 :startcolor => "#88ff88" ,
                 :endcolor => "#114411"

 

指定“想要施加可视化效果的元素”:只要把:current_item 传递给page 就行了。

 

隐藏空购物车

<% unless cart.items.empty ? %>
<div class="cart-title" >Your Cart</div>

……

<% end %>

 

script.aculo.us提供了几个可视化效果,可以漂亮地让页面元素出现在浏览器上。我们现在来试试blind_down ,它会让购物车平滑地出现,并让边框上其他的部分依次向下移动。 

 

显示购物车

depot_n/app/views/store/add_to_cart.js.rjs
page.replace_html("cart" , :partial => "cart" , :object => @cart)
page[:cart].visual_effect :blind_down if @cart.total_items == 1
page[:current_item].visual_effect :highlight,
               :startcolor => "#88ff88" ,
               :endcolor => "#114411" 

 

depot_n/app/models/cart.rb
def total_items
    @items.sum { |item| item.quantity }
end
 

 

隐藏购物车 

    <div id="cart"
     <% if @cart.items.empty? %>
      style="display: none"
     <% end %>

    >
     <%= render(:partial => "cart" , :object => @cart) %>
    </div>

 

辅助方法 

每当需要将某些处理逻辑从视图( 不管是哪种视图) 中抽象出来时,我们就应该编写一个辅助方法。 我们的辅助方法应该放在helpers目录下。进入这个目录,你会看到其中已经有几个文件存在了。

 

    <% hidden_div_if(@cart.items.empty? , :id => "cart" ) do %>
     <%= render(:partial => "cart" , :object => @cart) %>
    <% end %>
 

 

module StoreHelper
 def hidden_div_if(condition, attributes = {}, &block)
  if condition
   attributes["style" ] = "display: none"
  end
  content_tag("div" , attributes, &block)
 end
end

 

&block直接引向do..end

这段代码使用了Rails 的一个标准的辅助方法content_tag(),它把传递给它的代码块的输出用一个标记包装起来。通过&block 标志,我们可以将代码块经过hidden_div_if()方法传递给content_tag()方法。

 

JavaScript被禁用时的对策 

当用户点击form_remote_tag中的按钮时,可能会出现两种不同的情况。如果JavaScript被禁用,表单的目标action 就会被一个普通的HTFP POST请求直接调用——这就是一个普通的表单;如果允许使用JavaScript ,它就不会发起普通的POST 调用,而由一个JavaScript 对象和服务器建立后台连接一这个JavaScript 对象是XmlHTTPRequest 的实例,由于这个类的名字颇有点拗口,很多人( 以及Rails) 把它简称为xhr 。 

 

我们可以在服务器上检查进入的请求是否由xhr 对象发起的,从而判断浏览器是否禁用了JavaScript 。

 

depot_o/app/controllers/store_controller.rb
def add_to_cart
product = Product.find(params[:id])
@cart = find_cart
@current_item = @cart.add_product(product)
respond_to do |format|
→ format.js if request.xhr?
→ format.html {redirect_to_index}

end
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}" )
redirect_to_index("Invalid product" )
end

 

渐进式的Ajax 开发方式:首先从一个传统的应用程序开始,逐渐向其中增加Ajax 的特性。Ajax 可能很难调试;如果逐渐添加Ajax 特性,当遇到困难时,你也可以比较容易地找出问题所在。另外,正如我们看到的,从一个传统的应用程序开始也使你能够更容易地同时支持Ajax 和非Ajax 的行为方式。 

 

在开发过程中同时运行两个不同的浏览器会很有帮助:一个浏览器允许使用JavaScript ,另一个禁用。当添加新特性时,分别用这两个浏览器来检查,以确保不管是否允许使用JavaScript 都能使用这些功能。 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值