`

web开发敏捷之道2nd-rails进行web开发-笔记(全)

 
阅读更多

http://fsjoy.blog.51cto.com/318484/75615

http://fsjoy.blog.51cto.com/318484/75615 写道
说明:
开发环境配置:
OS:winxp
1.安装ruby版本:1.8.6 下载地址ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.6.zip
2.安装rails 版本:1.2.5 gem install rails -y
3.安装数据库:mysql- 5.0.22-win32 gem install mysql
--下载地址:
--http://download.mysql.cn/downloa ... ql-5.0.22-win32.zip
--修改一下安装路径C:\MySQL,剩下的安装默认安装即可
--配置环境变量,添加C:\MySQL\bin;

Case1.helloworld!
##这里必须要记住的是这三个CMD命令
1>rails demo #create一个rails应用
2.demo>ruby script/server #启动这个rails应用
3.demo>ruby script/generate controller Say #新建一个控制器
在控制器的脚本中写一个action,(hello),理解一下controller和view的关系,controller invokes view(控制器调用视图)
然后在view里面就生成了一个hello.rhtml。代码:
1 <html>
2 <head>
3 <title>hello, rails!</title>
4 </head>
5 <body>
6 <h1>Hello from Rails!</h1>
7 </body>
8 </html>

链接:
再创建goodbye的action,类似的编写goodbye.rhml代码
在hello.rhtml里加入下面的Ruby代码:
<p>Time to say<%=link_to "Goodbye!", :action =>"goodbye" %></p>
:action是一个ruby的symbol,看做”名叫某某某的东西”,因此:action就代表”名叫action”的东西(名片)
<%= ...%>写ruby的地方。
这样就链接起两个页面

动态模板的创建
1. 构建器Builder【以后讲】
2. 将Ruby代码嵌入到模板中。
.rhtml后缀告诉Rails,需要借助ERb系统对文件内容进行扩展---ERb就是用于将Ruby代码嵌入模板文件中。
普通内容进入rhtml直进直出,没有变化,<%=…%>符号之间的内容是Ruby代码,执行结果被转换成字符串,并替换<%...%>所在位置的内容。譬如在hello.rhtml中加入如下代码
1 <html>
2 <head>
3 <title>hello, rails!</title>
4 </head>
5 <body>
6 <h1>Hello from Rails!</h1>
7 <ul> #ul绘制文本的项目符号列表。
8 <li>Addition: <%=1+2%></li> #li引起列表中的一个项目。
9 <li>Concatenation:<%="cow" + "boy"%> </li>
10 <li>Time in one hour :<%= 1.hour.from_now%></li>
11 </ul>
12 </body>
13 </html>

# 如果是<%...%>就是Ruby代码的执行,如果加上等号最后就转换成了一段string。
1 <%3.times do%>
2 ho!<br/> #<br/>插入一个换行符。
3 <%end%>
Html源文件显示:

在<%...%>改变成<%...-%>
1 <%3.times do-%>
2 ho!<br/>
3 <%-end%>
Html源文件显示

但是在浏览器里不会有变化,一直都是这样显示

动态内容放入HTML页面中时应该使用h()方法
例如:Email :<%= h("Ann & Bill <frazers@isp.email>")%>
h()方法保护了邮件地址中的特殊字符不会扰乱浏览器显示
3. rjs 为现有页面添加Ajax魔法【以后讲】
学习总结:
 如何新建一个Rails应用程序,以及如何在其中新建控制器
 Rails是如何将用户请求映射到程序代码的
 如何在控制器中创建动态内容,以及如何在视图模板中显示动态内容
 如何把各个页面链接起来
depot项目迭代开发
增量式开发(Incremental Development),快速迭代思想,这个地方首先阅读一些敏捷开发和快速迭代的资料。
任务A:
 迭代A1.跑起来再说
 迭代A2.添加缺失字段
 迭代A3.检查一下(逻辑验证)
 迭代A4.更完美的列表页

迭代1:跑起来再说
 用脚手架,跑起来一个卖家的应用,可以添加数据…

>rails depot #创建rails应用depot

depot>mysqladmin -u root create depot_development #创建数据库

depot>ruby script/generate model product #创建product模型类

创建模型类的同时会创建这两个文件:
迁移任务 : 001_create_products.rb
模型类 :product.rb
#说明:迁移任务的名字:描述自己的功能(create_products[自动加了复数]),序列号 (001),扩展名(.rb)。
打开db/migrate目录下的001_create_products.rb,添加代码来创建数据库表(products表的内容)
1 class CreateProducts < ActiveRecord::Migration
2 def self.up # up()方法用于实施迁移
3 create_table :products do |t|
4 # t.column :name, :string #定义数据库表的代码
5
6 t.column :title, :string
7 t.column :description, :text
8 t.column :image_url, :string
9 end
10 end
11
12 def self.down # down()方法负责撤销up()方法的效果当需要把数据库恢复到前一个版本时13 就会执行这个方法
14 drop_table :products
15 end
16 end

depot>rake db:migrate #数据迁移

products表会被添加到database.yml文件的development
schema_info这张表的作用就是跟踪数据库的版本。如果要撤销数据迁移,可以用这句:rake db:migrate version=0这里0就恢复到了没有做过数据迁移的情况,1,2,3,4…根据实际情况使用。
depot>ruby script/generate controller admin #创建卖家控制器admin
------------------------------------
在admin这个控制器中加上脚手架(scaffold)
-------------------------------------
1 class AdminController < ApplicationController
2 scaffold :product #scaffold 运行时生成应用程序代码 :product模型来维护数据。
3 end
----------------------------------------
depot>ruby script/server #启动rails应用
http://localhost:3000/admin
迭代2.添加缺失的字段
 任务: 在products表中添加缺失的字段price,price是浮点类型,8位有效数字,保留2位有效数字,默认值是0.0
depot>ruby script/generate migration add_price #新建迁移任务,添加price字段
002_add_price.rb
1 class AddPrice < ActiveRecord::Migration
2
3 def self.up
4
5 add_column :products, :price, :decimal, :precision =>8, :scale=>2, :default=>0
6 # ↑ ↑ ↑ ↑ ↑
7 # 表的名字 字段名 类型 有效数字 保留两位有效数字
8 end
9
10 def self.down
11 remove_column :products, :price
12 end
13 end

column(name, type, options = {})
Instantiates a new column for the table. The type parameter must be one of the following values: :primary_key, :string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean.
Available options are (none of these exists by default):
• :limit: Requests a maximum column length (:string, :text, :binary or :integer columns only)
• :default: The column‘s default value. Use nil for NULL.
• :null: Allows or disallows NULL values in the column. This option could have been named :null_allowed.
• :precision: Specifies the precision for a :decimal column.
• :scale: Specifies the scale for a :decimal column.
Please be aware of different RDBMS implementations behavior with :decimal columns:
• The SQL standard says the default scale should be 0, :scale <= :precision, and makes no comments about the requirements of :precision.
• MySQL: :precision [1..63], :scale [0..30]. Default is (10,0).
• PostgreSQL: :precision [1..infinity], :scale [0..infinity]. No default.
• SQLite2: Any :precision and :scale may be used. Internal storage as strings. No default.
• SQLite3: No restrictions on :precision and :scale, but the maximum supported :precision is 16. No default.
• Oracle: :precision [1..38], :scale [-84..127]. Default is (38,0).
• DB2: :precision [1..63], :scale [0..62]. Default unknown.
• Firebird: :precision [1..18], :scale [0..18]. Default (9,0). Internal types NUMERIC and DECIMAL have different storage rules, decimal being better.
• FrontBase?: :precision [1..38], :scale [0..38]. Default (38,0). WARNING Max :precision/:scale for NUMERIC is 19, and DECIMAL is 38.
• SqlServer?: :precision [1..38], :scale [0..38]. Default (38,0).
• Sybase: :precision [1..38], :scale [0..38]. Default (38,0).
• OpenBase?: Documentation unclear. Claims storage in double.
This method returns self.
Examples
# Assuming td is an instance of TableDefinition
td.column(:granted, :boolean)
#=> granted BOOLEAN

td.column(:picture, :binary, :limit => 2.megabytes)
#=> picture BLOB(2097152)

td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
#=> sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL

def.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
#=> bill_gates_money DECIMAL(15,2)

def.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
#=> sensor_reading DECIMAL(30,20)

# While <tt>:scale</tt> defaults to zero on most databases, it
# probably wouldn't hurt to include it.
def.column(:huge_integer, :decimal, :precision => 30)
#=> huge_integer DECIMAL(30)


depot>rake db:migrate #迁移数据

depot>ruby script/server #启动depot
---------------
迭代A3,检查一下(加入验证逻辑)
任务:给输入数据加上验证
1. 非空字段:title, description, img_url;
2. 必须是数字的字段: price
3. price字段的值大于0
4. title的唯一性
5. img_url的格式验证:必须是png, jpg, bmp,gif结尾的字符串

models/product.rb
---------
1 class Product < ActiveRecord::Base
2
3 validates_presence_of :title, :description, :image_URL
4 validates_numericality_of :price #验证price是否是数字
5 validates_uniqueness_of :title #验证title字段在表中是否唯一
6 validates_format_of :image_url, #验证图片格式是否正确
7 :with => /\.(gif|jpg|png)$/i,
8 :message =>"must be a URL for a GIF, JPG or PNG image!"
9 protected #protected,是因为该方法必须在特定的模型上下文中调用,不能在外部随便调用
10 def validate #validate()的方法,在保存product实例前自动调用这个方法用它来检查字段合法性
11 errors.add(:price, "should be at least 0.01") if price.nil? || price<0.01
12 #先应该检查一下它是不是nil。如果是nil与0.01进行比较,就会发生一个异常。
13 end
14 end
----------------------------------------------------------------------------------------------------------------------------------------------

Protected Instance methods
validate( )
Overwrite this method for validation checks on all saves and use errors.add(field, msg) for invalid attributes.
迭代A4:更美观的列表页
任务 1. 把动态脚手架变成静态脚手架
2.编辑静态脚手架生成的代码
3.利用数据迁移加入测试数据.
4.加入css
depot>ruby script/generate scaffold product admin #生成静态的脚手架

overwrite app/controllers/admin_controller.rb? [Ynaqd] y
force app/controllers/admin_controller.rb
overwrite test/functional/admin_controller_test.rb? [Ynaqd] y
force test/functional/admin_controller_test.rb
用IDE(NetBeans)时,注意要写上model name ,controller name. When File Alreay Exist 在overwrite上选择
App/views/admin/list.rhtml文件中rails视图生成了当前的货品列表页面。
测试数据:
1.用可控的方式来填充数据,使用迁移任务。
depot>ruby script/generate migration add_test_data
修改003_add_test_data.rb:(/depot_c/db/migrate/003_add_test_data.rb)
顺便把图片和depot.css也拷贝到public\image和public\stylesheets目录下!
depot>rake db:migrate #执行数据迁移
引用CSS样式!(要看到css样式,需要重新启动应用):
views\layouts\admin.rhtml
<%= stylesheet_link_tag 'scaffold' ,'depot' %> #添加depot.css
接下来就是修改/app/views/admin/list.rhtml了!
拷贝一下
现在在浏览器中输入http://localhost:3000/admin/list就可以看到

总结:
 创建了开发数据库,并对Rails应用进行配置,使之能够访问开发数据库
 用迁移任务创建和修改了开发数据库的结构,并向其中装载了测试数据
 创建了Products表,并用脚手架生成器编写了一个应用程序,用于维护表中保存的货品信息
 在自动生成的代码基础上增加了对输入的验证
 重写了自动生成的视图代码,使之更美观
任务B:分类显示(将货品列表界面加以美化)
 迭代B1.创建分类列表
 迭代B2.添加页面布局
 迭代B3.用辅助方法格式化价格
 迭代B4.链接到购物车
迭代B1.创建分类列表
前面创建了Admin控制器,卖主用它来管理Depot应用
现在,创建第二个控制器Store,与付钱的买主交互的控制器.
generate controller store index #创建控制器store和一个action(index)
store_controller.rb
1 def index
2 @products=Product.find_products_for_sale
3 end
这里调用Product.find_products_for_sale, 调用了Product的一个类方法,所以在Model里知道product.rb进行定义此方法。
问:models里的文件都是对应的类,为什么创建model类的同时会自动创建数据库呢?
答:以后单独创建类文件的时候可以不用generate,直接新建一个类文件到model里!
models/product.rb

1 def self.find_products_for_sale
2 find(:all, :order=>"title")
3 end
#这里使用了Rails提供的find()方法,:all参数代表我们希望取出符合指定条件的所有记录。通过询问用户,我们确定了关于“如何对列表进行排序”,先按照货品名称排序,看看效果。于是:order=>”title”
find(*args)
Find operates with three different retrieval approaches:
• Find by id: This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]). If no record can be found for all of the listed ids, then RecordNotFound will be raised.
• Find first: This will return the first record matched by the options used. These options can either be specific conditions or merely an order. If no record can matched, nil is returned.
• Find all: This will return all the records matched by the options used. If no records are found, an empty array is returned.
All approaches accept an option hash as their last parameter. The options are:
• :conditions: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro.
• :order: An SQL fragment like "created_at DESC, name".
• :group: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
• :limit: An integer determining the limit on the number of rows that should be returned.
• :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
• :joins: An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed). The records will be returned read-only since they will have attributes that do not correspond to the table‘s columns. Pass :readonly => false to override.
• :include: Names associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer to already defined associations. See eager loading under Associations.
• :select: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not include the joined columns.
• :from: By default, this is the table name of the class, but can be changed to an alternate table name (or even the name of a database view).
• :readonly: Mark the returned records read-only so they cannot be saved or updated.
• :lock: An SQL fragment like "FOR UPDATE" or "LOCK IN SHARE MODE". :lock => true gives connection‘s default exclusive lock, usually "FOR UPDATE".
Examples for find by id:
Person.find(1) # returns the object for ID = 1
Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
Person.find([1]) # returns an array for objects the object with ID = 1
Person.find(1, :conditions => "administrator = 1", :order => "created_on DESC")
Examples for find first:
Person.find(:first) # returns the first object fetched by SELECT * FROM people
Person.find(:first, :conditions => [ "user_name = ?", user_name])
Person.find(:first, :order => "created_on DESC", :offset => 5)
Examples for find all:
Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
Person.find(:all, :offset => 10, :limit => 10)
Person.find(:all, :include => [ :account, :friends ])
Person.find(:all, :group => "category")
Example for find with a lock. Imagine two concurrent transactions: each will read person.visits == 2, add 1 to it, and save, resulting in two saves of person.visits = 3. By locking the row, the second transaction has to wait until the first is finished; we get the expected person.visits == 4.
Person.transaction do
person = Person.find(1, :lock => true)
person.visits += 1
person.save!
end
#find()方法返回一个数组,其中每个元素是一个Product对象,分别代表从数据库返回的一条记录。find_products_for_sale()方法直接把这个数组返回给控制器。
views/store/index.rhtml
1 <h1>Your Pragmatic Catalog</h1> #标题
2 <%for product in @products -%> #ruby语句,循环货品
3 <div class='entry'>
4 <img src="<%=product.image_url%>"/>
5 <h3><%=h(product.title)%></h3>
6 <%=product.description%>
7 <span class="price"><%=product.price%></span>
8 </div>
9 <%end%>

迭代B2.添加页面布局
任务:
添加一个布局文件
布局(layout)是一个模板,可以将内容填充到模板。可以把所有的在线商店页面定义同样的布局,然后把“分类列表”的页面塞进这个布局里,稍后,我们可以把“购物车”和“结账”的页面也套进同样的布局。因为只有一个布局模板,我们只需要修改一个文件,就可以改变整个站点的观感。
在Rails中,定义和使用布局的方式有好多种,我们现在使用最简单的一种,如果在/app/views/layouts目录中创建一个与某个控制器同名的模板文件,那么该控制器所渲染的视图在默认状态下会使用此模板布局。所以下面就来创建这样一个模板。创建与store同名的模板文件store.rhtml
views/layouts/store.rhtml
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3 <html>
4 <head>
5 <title>Pragprog Books Online Store</title>
6 <%= stylesheet_link_tag "depot", :media => "all" %>
7 </head>
8 <body id="store">
9 <div id="banner">
10 <%= image_tag("logo.png") %>
11 <%= @page_title || "Pragmatic Bookshelf" %>
12 </div>
13 <div id="columns">
14 <div id="side">
15 <a href="http://www....">Home</a><br />
16 <a href="http://www..../faq">Questions</a><br />
17 <a href="http://www..../news">News</a><br />
18 <a href="http://www..../contact">Contact</a><br />
19 </div>
20 <div id="main">
21 <%= yield :layout %>
22 </div>
23 </div>
24 </body>
25 </html>
重新启动rails应用

迭代B3.用辅助方法格式化价格
 将数据库中以数字形式保存的价格变成“元+分”的形式,例如值为12.34的价格应该显示为$12.34,”13”应该显示为”$13.00”
可以这样进行格式化:
view/index.rhtml
<span class =”price”><%=sprintf(“$%0.2f”, product.price)%></span>
但是,如果我们以后对应用程序进行国际化,会给维护带来麻烦。用一种单独的辅助方法来处理“价格格式化”的逻辑。view/index.rhtml
<span class="price"><%=number_to_currency(product.price)%></span>
number_to_currency(number, options = {})
Formats a number into a currency string. You can customize the format in the options hash.
• :precision - Sets the level of precision, defaults to 2
• :unit - Sets the denomination of the currency, defaults to "$"
• :separator - Sets the separator between the units, defaults to "."
• :delimiter - Sets the thousands delimiter, defaults to ","
number_to_currency(1234567890.50) => $1,234,567,890.50
number_to_currency(1234567890.506) => $1,234,567,890.51
number_to_currency(1234567890.506, :precision => 3) => $1,234,567,890.506
number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "")
=> £1234567890,50
迭代B4.链接到购物车
 任务:添加一个Add to cart 按钮,可以把货品添加到购物车
index.rhtml中添加语句:
<%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
--help
button_to(name, options = {}, html_options = {})
Generates a form containing a single button that submits to the URL created by the set of options. This is the safest method to ensure links that cause changes to your data are not triggered by search bots or accelerators. If the HTML button does not work with your layout, you can also consider using the link_to method with the :method modifier as described in the link_to documentation.
The generated FORM element has a class name of button-to to allow styling of the form itself and its children. You can control the form submission and input element behavior using html_options. This method accepts the :method and :confirm modifiers described in the link_to documentation. If no :method modifier is given, it will default to performing a POST operation. You can also disable the button by passing :disabled => true in html_options.
button_to "New", :action => "new"
Generates the following HTML:
<form method="post" action="/controller/new" class="button-to">
<div><input value="New" type="submit" /></div>
</form>
If you are using RESTful routes, you can pass the :method to change the HTTP verb used to submit the form.
button_to "Delete Image", { :action => "delete", :id => @image.id },
:confirm => "Are you sure?", :method => :delete
Which generates the following HTML:
<form method="post" action="/images/delete/1" class="button-to">
<div>
<input type="hidden" name="_method" value="delete" />
<input confirm('Are you sure?');"
value="Delete" type="submit" />
</div>
</form>

总结:
 新建一个控制器,用于处理买主的交互
 实现默认的index() action
 在Product模型类中添加一个类方法,使之返回所有可销售的货品项
 实现一个视图(rhtml文件)和一个布局模板(同样是rhtml文件),用于显示货品列表
 创建一个简单的样式表
 使用辅助方法对价格信息进行格式化
 给每件货品增加一个按钮,让用户可以将其放入自己的购物车
任务C:创建购物车
session
 迭代C1:创建购物车
 迭代C2:更聪明的购物车
 迭代C3:处理错误
 迭代C4:结束购物车
把session放在数据库中
Rails可以很容易地把session数据保存在数据库中。用Rake任务创建所需要的数据库表
1.rake db:sessions:create
数据迁移
2.rake db:migrate
3.在config/environment.rb文件中找到
# config.action_controller.session_store = :active_record_store
把#去掉,就会激活基于数据库的session存储机制
购物车和session
在strore控制器中加入方法find_cart()方法
1 def find_cart
2 unless session[:cart] #if there’s no cart in the session
3 session[:cart]=Cart.new #add a new one
4 end
5 session[:cart] #return existing or new cart
6 end
在控制器中使用当前session就像使用一个hash一样—:cart是一个symbol,作为索引。而且Cart是一个类。
更常用的方式:
1 private
2 def find_cart
3 session[:cart]||=Cart.new # Ruby的条件赋值操作符 ||=.
4 end
||=的作用:如果session的hash中已经有:cart这个键,上述语句会立即返回:cart键对应的值;否则新建一个Cart对象,并放入session,并返回新建的对象。
#注意,这里声明的private , Rails不会将其作为控制器中的一个action, 此外要小心的---如果把这些方法都放在private声明的后面,从控制器之外就看不到它们,新的action必须位于private之前!
迭代C1:创建购物车
已经创建了session来存储购物车了,购物车是个类,这个类有一些数据,再加上一些业务逻辑,所以从逻辑上来说应该是一个模型对象。这里,不需要一张数据库表与之对应了。因为购物车与买家session紧密相关,而session数据又是所有服务器都可以访问到的(应用程序可能会部署在多服务器的环境下),所以完全不必再额外存储购物车了。因此先用一个普通的类来实现购物车,看看有什么效果,这个类实现:目前它只是包装了一个数组,其中存放一组货品。当顾客往购物车中添加货品(用add_product()方法)时,选中的货品就会被添加到数组之中。
创建一个cart.rb(不是generate),放在model目录下
1 class Cart
2 attr_reader :items #@items可读写属性打开
3
4 def initialize #构造函数
5 @items=[] #初始化数组@items
6 end
7
8 def add_product(product) #定义加入购物车货品
9 @items<<product #将货品id加入购物车
10 end
11
12 def total_price #定义总价格的方法
13 @items.sum { |item| item.price } #数组的sum方法
14 end
15 end

前面在“分类列表”视图已经为每种货品提供了“Add to Cart”链接
<%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
这个链接指向store 控制器的 add_to_cart() action方法(这个方法暂时还不存在),并传入货品的ID作为参数(:id=>product实际上是:id=>product.id的缩写,两者都会将货品的id传回给控制器)
模型中的id字段的重要性:rails根据这个字段来识别模型对象的身份,以及它们对应的数据库表。只要将一个id传递给add_to_cart()方法,我们就可以唯一地标志要加入购物车的货品。

写add_product方法,在store_controller.rb中,写在private上面(action方法)
controllers/store_controller.rb
1 def add_to_cart
2 @cart=find_cart #从session中找出购物车对象(或者新建一个)
3 product=Product.find(params[:id]) #利用params对象从请求中取出id参数,随后请求Product模型
4 #根据id找出货品
5 @cart.add_product(product) #把找出的货品放入购物车
6 end
params 是Rails应用中一个重要的对象,其中包含了浏览器请求传来的所有参数。按照惯例,params[:id]包含了将被action使用的对象id(或者说主键) 。在视图中调用button_to时我们已经使用:id=>product把这个值设置好了
再写add_to_cart这个action对应的rhtml
代码:
1 <h1>Your Pragmatic Cart</h1>
2
3 <ul>
4 <%for item in @cart.items%> #在购物车数组里执行循环体
5 <li><%=h(item.title)%></li> #显示购物车里或货品的title
6 <%end%>
7
8 </ul>
运行看看起来
迭代C2.更聪明的购物车
在model下新建一个ruby文件,cart_item.rb,其中引用一种货品,并包含数量信息。(新建一个类就要新建一个model下的ruby文件)
1 class CartItem
2 attr_reader :product, :quantity

3 def initialize(product) #构造函数
4 @product=product
5 @quantity=1
6 end
7
8 def increment_quantity
9 @quantity+=1
10 end
11
12 def title
13 @product.title
14 end
15
16 def price #计算总价
17 @product.price*@quantity
18 end
19 end
对models/cart.rb修改add_product()方法

1 def add_product(product)
2 current_item=@items.find{|item| item.product==product} #查看物品列表中是否已经有了product
3 if current_item #如果已经存在
4 current_item.increment_quantity #物品数量+1
5 else #如果不存在
6 @items<<CartItem.new(product) #新增一个CartItem对象
7 end
8 end
另外,对add_to_cart视图做一个简单修改,将“数量”显示出来
1 <h1>Your Pragmatic Cart</h1>
2
3 <ul>
4 <%for cart_item in @cart.items%>
5 <li><%=cart_item.quantity%> × <%=h(cart_item.title)%></li>
6 <%end%>
7
8 </ul>
这时会跳出一个错误页面来
原因:程序认为购物车中的东西是Product对象,而不是CartItem对象,好像Rails压根就不知道已经修改了Cart类。
购物车对象从session中来,而session中的购物车对象还是旧版本的:它直接把货品放进@items数组,所以当Rails从session中取出这个购物车之后,里面装的都是Product对象,而不是CartItem对象。
解决方法:删除旧的session,把原来那个购物车留下的所有印记都抹掉,由于我们使用数据库保存session数据,只要一个rake命令就可以清空session表。
Rake db:sessions:clear
现在执行Rails应用。
迭代C3.处理错误
问题描述:现在有一个程序bug当在浏览器中敲入http://.../store/add_to_cart/id,id如果不在货品id范围内,会引发一个错误页面。
解决途径:
Flash 结构—所谓flash其实就是一个篮子(与hash非常类似),你可以在处理请求的过程中把任何东西放进去。在同一个session的下一个请求中,可以使用flash的内容,然后这些内容就会被自动删除。一般来说,flash都是用于收集错误信息的,譬如:当add_to_cart() action发现传入的id不合法时,它就会将错误信息保存在flash中,并重定向到index() action,以便重新显示分类列表页面。Index action的视图可以将flash中的错误信息提取出来,在列表页面顶端显示。在视图中,使用flash方法即可访问flash中的信息
不用普通实例变量保存错误信息的原因:重定向指令是由应用程序发送给浏览器,后者接收到该指令后,再向应用程序发送一个新的请求。当应用程序收到后一个请求时,前一个请求中保存的实例变量早就消失无踪了。Flash数据是保存在session中的,这样才能够在多个请求之间传递。
对add_to_cart()方法进行修改,拦截无效的货品id,并报告这一错误:
controller/store_controller.rb
1 def add_to_cart
2 begin
3 product=Product.find(params[:id])
4 rescue ActiveRecord ::RecordNotFound # rescue 子句拦下了 Product.find()抛出的异常
5 logger.error("Attempt to access invalid product #{params[:id]}") #写入日志,级别是error
6 flash[:notice] = "Invalid product" #创建flash信息解释出错的原因,跟hash一样,:notice作为键
7 redirect_to :action => "index" #让浏览器重新定向
8 else #没有异常发生时
9 @cart=find_cart
10 @cart.add_product(product)
11 end
12 end
启动Rails应用看一下结果,当在浏览器后面不是货品里的id时,代码就会生效,日志里可以查找到Attempt to access invalid product xx。
这里日志生效了,但是没有把flash信息显示在浏览器里。
在layouts中修改store.rhml
1 <div id="main">
2 <%if flash[:notice]%> #如果flash里有内容
3 <div id="notice"><%=flash[:notice]%></div> #显示flash的内容
4 <%end%>
5 <%= yield :layout %>
6 </div>
迭代C4:结束购物车
任务:加入清空购物车功能,用一个button_to()方法添加一个empty_cart()方法
add_to_cart.rhtml加入下面语句
<%=button_to "Empty cart", :action=>:empty_cart %>
这里有:action=>:empty_cart, empty_cart()这个action要实现的功能是将购物车从session中去掉,并在flash中设置提示信息,最后将浏览器重新定向到首页。
store_controller.rb
1 def empty_cart()
2 session[:cart]=nil #清空购物车
3 flash[:notice]="Your cart is currently empty" #flash中设置提示信息
4 redirect_to :action => "index" #重新定向首页
5 end
在store_controller.rb中注意到有两次调用到redirect_to方法,两次使用flash的内容显示信息。为了不出现重复代码可以修改一下
1 def add_to_cart
2 begin
3 product=Product.find(params[:id])
4 rescue ActiveRecord::RecordNotFound
5 logger.error("Attempt to access invalid product #{params[:id]}")
6 redirect_to_index("Invalid product")
7 else
8 @cart=find_cart
9 @cart.add_product(product)
10 end
11 end
12
13 def empty_cart()
14 session[:cart]=nil
15 redirect_to_index("Your cart is currently empty")
16 end
17 private
18 def redirect_to_index(msg)
19 flash[:notice]=msg
20 redirect_to :action => "index"
21 end
总结:
 使用session来存储状态
 创建并使用了与数据库无关的模型对象
 用flash在action之间传递错误信息
 用日志器记录事件
 用辅助方法消除重复代码
任务D:Ajax初体验
--Ajax介绍:
 Ajax不是一种技术。实际上,它由几种蓬勃发展的技术以新的强大方式组合而成。Ajax包含:
基于XHTML和CSS标准的表示;
使用Document Object Model进行动态显示和交互;
使用XMLHttpRequest与服务器进行异步通信;
使用JavaScript绑定一切。

 Ajax是由Jesse James Garrett创造的,是“Asynchronous JavaScript + XML的简写”。
  Ajax用来描述一组技术,它使浏览器可以为用户提供更为自然的浏览体验。在Ajax之前,Web站点强制用户进入提交/等待/重新显示范例,用户的动作总是与服务器的“思考时间”同步。Ajax提供与服务器异步通信的能力,从而使用户从请求/响应的循环中解脱出来。借助于Ajax,可以在用户单击按钮时,使用JavaScript和DHTML立即更新UI,并向服务器发出异步请求,以执行更新或查询数据库。当请求返回时,就可以使用 JavaScript和CSS来相应地更新UI,而不是刷新整个页面。最重要的是,用户甚至不知道浏览器正在与服务器通信:Web站点看起来是即时响应的。
  虽然Ajax所需的基础架构已经出现了一段时间,但直到最近异步请求的真正威力才得到利用。能够拥有一个响应极其灵敏的Web站点确实激动人心,因为它最终允许开发人员和设计人员使用标准的HTML/CSS/JavaScript堆栈创建“桌面风格的(desktop-like)”可用性。
  所有这些Web站点都告诉我们,Web应用程序不必完全依赖于从服务器重新载入页面来向用户呈现更改。一切似乎就在瞬间发生。简而言之,在涉及到用户界面的响应灵敏度时,基准设得更高了。
Ajax的工作原理
  Ajax的核心是JavaScript对象XmlHttpRequest。该对象在Internet Explorer 5中首次引入,它是一种支持异步请求的技术。简而言之,XmlHttpRequest使您可以使用JavaScript向服务器提出请求并处理响应,而不阻塞用户。
  在创建Web站点时,在客户端执行屏幕更新为用户提供了很大的灵活性。下面是使用Ajax可以完成的功能:
 动态更新购物车的物品总数,无需用户单击Update并等待服务器重新发送整个页面。
 提升站点的性能,这是通过减少从服务器下载的数据量而实现的。例如,在Amazon的购物车页面,当更新篮子中的一项物品的数量时,会重新载入整个页面,这必须下载32K的数据。如果使用Ajax计算新的总量,服务器只会返回新的总量值,因此所需的带宽仅为原来的百分之一。
 消除了每次用户输入时的页面刷新。例如,在Ajax中,如果用户在分页列表上单击Next,则服务器数据只刷新列表而不是整个页面。
 直接编辑表格数据,而不是要求用户导航到新的页面来编辑数据。对于Ajax,当用户单击Edit时,可以将静态表格刷新为内容可编辑的表格。用户单击Done之后,就可以发出一个Ajax请求来更新服务器,并刷新表格,使其包含静态、只读的数据。
  一切皆有可能!但愿它能够激发您开始开发自己的基于Ajax的站点。然而,在开始之前,让我遵循传统的提交/等待/重新显示的方式,然后讨论Ajax如何提升用户体验。
给购物车加上Ajax:
任务:把当前购物车显示到分类页面的边框里,而不要一个单独的购物车页面,然后加上Ajax魔法,只更新边框里的购物车,而不必重新刷新整个页面。
…要使用Ajax,先做一个非Ajax版本的应用程序,然后逐步引入Ajax功能。
迭代D1.迁移购物车
任务:将原先由add_to_cart和对应的rhtml模板来渲染的购物车的逻辑放到负责分类页面的布局模板中。使用局部模板
add_to_cart.rhtml
1 <div class="cart-title">Your Cart</div>
2 <table>
3 #render()方法接收两个参数,局部模板的名字和一组对象的集合
4 <%=render(:partial=>"cart_item", :collection=>@cart.items)%> #cart_item局部模板名
5 <tr class="total-line"> #tr指定表格中的一行。
6 <td colspan="2">Total</td> #td指定表格中的单元格。‘2’代表2个单元格
7 <td class="total-cell"><%=number_to_currency(@cart.total_price)%></td>
8 </tr>
9 </table>
10
11 <%=button_to "Empty cart", :action=>:empty_cart %>
局部模板本身就是另一个模板文件(默认情况下与调用它的模板文件位于同一个目录下)。为了从名字上将局部模板与普通模板区分开来,Rails认为局部模板的名字都以下划线开头,在查找局部模板时也会在传入的名字前面加上下划线,所以,我们的局部模板更add_to_cart.rhtml在同一目录下,名字是 _cart_item.rhtml
1 <tr> #指定表格中的一行
2 <td><%=cart_item.quantity%>×</td> #显示为2x
3 <td><%=h(cart_item.title)%></td> #显示货品的title
4 <td class="item-price"><%=number_to_currency(cart_item.price)%></td>
5 </tr>
注意:局部模板的名字是”cart_item”,所以在局部模板内部就会有一个名叫”cart_item”的变量。
现在已经对“显示购物车”的代码做出了整理,但还没有将它搬到边框里。要实现这次搬迁,就需要将这部分逻辑放到局部模板中。如果我们编写出一个用于显示购物车的局部模板,那么只要在边框里嵌入如下调用就可以了:render(:partial=>"cart")
但局部模板怎么知道哪里去获得购物车对象呢?还记得在add_to_cart 模板中用到的“渲染一组对象”的技巧么?给局部模板内部的cart_item变量设上了值。当直接调用局部模板时也可以做同样的事情:render()方法的:object参数接收一个对象作为参数,并将其赋给一个与局部模板同名的变量。所以在局部模板中,我们可以这样调用render()方法:
<%=render(:partial=>"cart", :object=>@cart)%>
这样在_cart.rhtml模板中就可以通过cart变量来访问购物车。
现在创建_cart.rhtml模板,它和add_to_cart模板大同小异,不过使用了cart变量而不是@cart变量。(注意:局部模板还可以调用别的局部模板)
_cart.rhtml
1 <div class="cart-title">Your Cart</div>
2 <table>
3 <%=render(:partial=>"cart_item", :collection=>cart.items)%>
4 <tr class="total-line">
5 <td colspan="2">Total</td>
6 <td class="total-cell"><%=number_to_currency(cart.total_price)%></td>
7 </tr>
8 </table>
9
10 <%=button_to "Empty cart", :action=>:empty_cart %>
现在修改store布局模板,在边框中加上新建的局部模板。

1 <div id="side">
2 <div id="cart">
3 <%=render(:partial=>"cart", :object=>@cart)%>
4 </div>
5 <a href="http://www....">Home</a><br />

现在我们必须对storeController稍加修改,因为在访问index这个action时会调用布局模板,但这个action并没有对@cart变量设值。
store_controller.rb
1 def index
2 @products=Product.find_products_for_sale
3 @cart = find_cart
4 end
现在查看一下页面
问题:点了 Add to Cart按钮就跳转到add_to_cart这个页面,不利于继续购物。
改变流程
购物车已经在边框中显示出来了,随后我们改变”add to cart”按钮的工作方式:它无须显示一个单独页面,只要刷新首页就行了。改变方法:在add_to_cart这个action方法的最后,直接把浏览器重定向到首页。
1 def add_to_cart
2 begin
3 product=Product.find(params[:id])
4 rescue ActiveRecord::RecordNotFound
5 logger.error("Attempt to access invalid product #{params[:id]}")
6 redirect_to_index("Invalid product")
7 else
8 @cart=find_cart
9 @cart.add_product(product)
10 redirect_to_index
11 end
12 end
为了能让这段代码工作,还需要修改def redirect_to_index()的定义,将消息这个参数变成可选的。
1 def redirect_to_index(msg=nil)
2 flash[:notice]=msg if msg
3 redirect_to :action => "index"
4 end
是时候去掉add_to_cart.rhtml模板了!
问题:这里用了重新定向页面,如果列表页面很大的话,会占用带宽和服务器资源。解决方案Ajax!
迭代D2.基于 Ajax的购物车
Ajax允许编写的代码在浏览器中运行,并与服务器的应用程序交互,在这里,让”Add to Cart”按钮在后台调用服务器add_to_cart方法,随后服务器把关于购物车的HTML发回浏览器,我们只要把服务器更新的HTML 片段替换到边框里就行了。
要实现这种效果,通常做法是首先编写在浏览器中运行的JavaScript代码,然后编写服务器代码与Javascript交互(可能是通过JSON之类的技术)。Good news:只要使用Rails,这些东西都将被隐藏起来:我们使用Ruby(再借助一些Rails辅助方法)就可以完成所有功能。
向应用程序中引入Ajax的技巧在于“小步前进”,所以我们先从最基本的开始:修改货品列表页,让它向服务器端应用程序发起Ajax请求:应用程序则应答一段HTML代码,其中展示了最新的购物车。
在索引页上,目前我们使用button_to()来创建“Add to cart”链接的。button_to()其实就生成了HTML的<form>标记。下列辅助方法
<%=button_to "Add to Cart", :action=>:add_to_cart, :id=>product%>
会生成类似这样的HTML代码:
<form method="post" action="/store/add_to_cart/1" class="button-to">
<div><input type="submit" value="Add to Cart" /></div>
</form>
这是一个标准的HTML表单,所以,当用户点击提交按钮时,就会生成一个post请求。我们不希望这样,而是希望它发送一个Ajax 请求。为此,必须直接地编写表单代码—可以使用form_remote_tag这个Rails辅助方法。”form_..._tag”这样的名字代表它会生成HTML表单,”remote”则说明它会发起Ajax远程调用。现在打开index.rhtml,将button_to()调用替换为下列代码:
1 <% form_remote_tag :url => {:action => :add_to_cart,:id=>product} do%>
2 <%=submit_tag "Add to Cart"%>
3 <%end%>
# 用:url参数告诉form_remote_tag()应该如何调用服务器端的应用程序。该参数接收一个hash,其中的值就跟传递给 button_to()方法的参数一样。在表单内部(也就是do和end之间的代码块中),我们编写了一个简单的提交按钮。在用户看来,这个页面就跟以前一摸一样。
虽然现在处理的是视图,但我们还需要对应用程序做些调整,让它把Rails需要用到的Javascript库发送到用户的浏览器上。现在需要在store布局的<head>部分里调用javascript_include_tag方法即可。
views/layouts/store.rhtml
1 <head>
2 <title>Pragprog Books Online Store</title>
3 <%= stylesheet_link_tag "depot", :media => "all" %>
4 <%= javascript_include_tag :defaults%>
5 </head>
浏览器已经能够向应用程序发起Ajax请求,下一步就是让应用程序做出应答。创建一段HTML代码来代表购物车,然后让浏览器把这段HTML代码插入到当前页面的DOM,替换掉当前显示的购物车。为此,要做的第一个修改就是不再让add_to_cart重新定向到首页。

store_controller.rb
#redirect_to_index
修改的结果是,当add_to_cart完成对Ajax请求的处理之后,Rails就会查照add_to_cart这个模板来执行渲染。但是我们给删掉这个模板了。
.rjs模板可以将Javascript发送到浏览器,需要写的只是服务器端的Ruby代码。
views/store/add_to_cart.rjs

1 page.replace_html ("cart",:partial=>"cart", :object=>@cart)

page 这个变量是JavaScript生成器的实例—这是Rails提供的一个类,它知道如何在服务器端创建Javascript,并使其在浏览器上运行。在这里,它找到当前页面上id为cart的元素,然后将其中的内容替换成…某些东西。传递给replace_html的参数看上去很眼熟,因为它们就跟 store布局中渲染局部模板时传入的参数完全一样。这个简单的.rjs模板就会渲染出用于显示购物车的HTML代码,随后就会告诉浏览器将 id=”cart”的<div>中的内容替换成这段HTML代码。

迭代D3.高亮显示购物车变化
javascript_include_tag辅助方法会把几个JavaScript库加到浏览器中,其中之一effects.js可以给页面装饰以各种可视化效果。其中有“黄渐变技巧”高亮显示。
问题:要标记出其中最近的一个更新。先对cart模型类做调整;让add_product()方法返回与新加货品对应的CartItem对象。
models/cart
1 def add_product(product)
2 current_item=@items.find{|item| item.product==product}
3 if current_item
4 current_item.increment_quantity
5 else
6 current_item = CartItem.new(product)
7 @items<<current_item
8 end
9 current_item
10 end
在store_controller.rb中,我们可以提取这项信息,并将其赋给一个实例变量,以便传递给视图模板。
controllers/store_controller.rb
1 def add_to_cart
2 begin
3 product=Product.find(params[:id])
4 rescue ActiveRecord::RecordNotFound
5 logger.error("Attempt to access invalid product #{params[:id]}")
6 redirect_to_index("Invalid product")
7 else
8 @cart=find_cart
9 @current_item=@cart.add_product(product)
10 end
11 end
在_cart_item.rhtml局部模板中,我们会检查当前要渲染的这种货品是不是刚刚发生了变化,如果是,就给它做个标记:将它的id属性设为currtent_item.
1 <% if cart_item== @current_item%>
2 <tr id="current_item">
3 <%else%>
4 <tr>
5 <%end%>
6 <td><%=cart_item.quantity%>×</td>
7 <td><%=h(cart_item.title)%></td>
8 <td class="item-price"><%=number_to_currency(cart_item.price)%></td>
9 </tr>
经过这三个修改之后,最近发生变化的货品所对应的<tr>元素会被打上id=”current_item”的标记。现在,我们只要告诉 JavaScript对这些元素施加高亮效果就行了:在add_to_cart.rjs模板中调用visual_effect方法。
1 page.replace_html("cart", :partial => "cart", :object => @cart)
2
3 page[:current_item].visual_effect :highlight,
4 :startcolor => "#88ff88",
5 :endcolor => "#114411"
现在浏览页面即可看到效果。
迭代D4.隐藏购物车
任务:购物车里没有东西的时候不显示购物车。有东西时候才显示购物车
简单做法:当购物车中有东西才包含显示它的那段HTML代码。只需修改_cart一个局部模板
<% unless cart.items.empty?%>

<%end%>
虽然这是一个可行方案,但是却让UI显示有些生硬:当购物车由空转为不空时,整个边框都需要重绘。所以,不这么做,还是做的圆滑一点吧!
Script.aculo.us提供了几个可视化效果,可以漂亮地让页面元素出现在浏览器上。试试blind_down,它会让购物车平滑地出现,并让边框上其他部分依次向下移动。
在 add_to_cart模板中实现,因为add_to¬_cart这个模板只有在用户向购物车中添加货品时才会被调用,所以我们知道:如果购物车中有了一件货品,那么就应该在边框中显示购物车了(因为这就意味着此前购物车是空的,因此也是隐藏起来的)。此外,我们需要先把购物车显示出来,然后才使用背景高亮的可视化效果,所以“显示购物车”的代码应该在“触发高亮”代码之前
add_to_cart.rjs
page.replace_html("cart", :partial => "cart", :object => @cart)
#START_HIGHLIGHT
page[:cart].visual_effect :blind_down if @cart.total_items == 1
#END_HIGHLIGHT
page[:current_item].visual_effect :highlight,
:startcolor => "#88ff88",
:endcolor => "#114411"
这里@cart.total_items,total_item方法没有定义,在model/cart.rb中定义此方法
def total_items
@items.sum{|item| item.quantity}
end

现在购物车为空的时候还需要将它隐藏起来,其他已经很圆滑的实现了视觉效果。
两个方法:1开始展示的,购物车为空的时候不显示html代码。但是,当用户朝空的购物车放入第一件货品的时候,浏览器会出现一阵闪烁---购物车被显示出来,然后又隐藏起来,然后再被blind_down效果逐渐显示出来
所以,更好的方法2.创建“显示购物车”的html,但对它的CSS样式进行设置,使其不被显示出来—如果购物车为空,就设置为display:none.为此,需要修改layouts/store.rhtml文件,首先的方法:

<div id="cart"
<%if @cart.items.empty?%>
style="display:none"
<%end%>
>
<%=render(:partial=>"cart", :object=>@cart)%>
当购物车为空时,这段代码就会给<div>标记加上style="display:none"这段CSS属性。这确实有效,不过真的很丑陋,下面挂着一个孤零零的”>”字符,看起来就像放错了地方(虽然确实没放错);更糟糕的是,逻辑放到了HTML标记的中间,这正是给模板语言带来恶名的行为。不要让这么丑陋的代码出现,我们还是创建一层抽象—编写一个辅助方法把它隐藏起来吧!
辅助方法
每当需要将某些处理逻辑从视图(不管是哪种视图)中抽象出来时,我们就可以编写一个辅助方法。
在 app目录下,有一个helpers的子目录,我们的辅助方法就放在这个子目录下,Rails代码生成器会自动为每个控制器(这里就是store和 admin)创建一个辅助方法文件;Rails命令本身(也就是一开始创建整个应用程序的命令)则生成了该控制器引用的视图都可以使用该方法;如果把方法放在application_helper.rb文件中,则整个应用程序的所有视图都可以使用它。所以我们就需要给新建的辅助方法找一个安身之所了:目前还只有store控制器的视图中需要使用它,所以就放在store_helper.rb文件中吧。
在store_helper.rb这个文件中,默认代码是:
module StoreHelper
end
我们来编写一个名叫hidden_div_if()的辅助方法,它接收两个参数:一个条件判断,以及(可选的)一组属性。该方法会创建一个<div>标记,如果条件判断为true,就给它加上display:none样式。在布局文件中,我们这样使用它:/layouts /store.rhtml:
<div id="columns">
<div id="side">
<% hidden_div_if(@cart.items.empty?, :id => "cart") do %>
<%= render(:partial => "cart", :object => @cart) %>
<% end %>

<a href="http://www....">Home</a><br />
这里的辅助方法hidden_div_if()写在store_helper.rb中,这样就只有storeController能够使用它。
module StoreHelper

def hidden_div_if(condition, attributes={})
if condition
attributes["style"]="display: none"
end
attrs=tag_options(attributes.stringify_keys)
"<div #{attrs}>"
end
end
#我们从Rails提供的content_tag()辅助方法中抄袭了一段代码,才知道可以这样调用tag_options()方法。
目前当用户清空购物车时,会显示一条flash消息。现在要把这条消息去掉了—我们不再需要它了!因为当货品列表页刷新时,购物车直接就消失了。除此之外,还有另一个原因要去掉它:我们已经用Ajax来向购物车中添加货品,用户在购物时主页面就不会刷新;也就是说,只要用户清空了一次购物车,那条flash 消息就会一直显示在页面上,告诉他“购物车已经被清空”,即便边框里显示的购物车又被放进了新的货品。
Store_controller.rb

def empty_cart()
session[:cart]=nil
redirect_to_index
end
看起来步骤不少,其实不然:要隐藏和显示购物车,我们所需要做的只是根据其中货品的数量设置css的显示样式,另外,当第一件货品被放进购物车时,通过.rjs模板来调用blind_down效果,仅此而已。
迭代D5:JavaScript被禁用的对策
如果针对add_to_cart的请求不是来自JavaScript,我们就希望应用程序仍然采用原来的行为,并将浏览器重新定向到首页。当首页显示出来时,更新之后的购物车就会出现在边栏上了。
当用户点击form_remote_tag中的按钮时,可能出现两种不同情况。如果JS被禁用,表单的目标action就会被一个普通的HTTP POST请求直接调用—这就是一个普通表单;如果允许使用JS,它就不会发起普通POST调用,而由一个JS对象和服务器建立后台连接—这个JS对象是 XmlHTTPRequest的实例,由于这个类的名字拗口,很多人(以及Rails)把它简称为xhr。
所以,我们可以在服务器上检查进入的请求是否由xhr对象发起,从而判断浏览器是否禁用了JS。Rails提供的request对象—在控制器和视图中都可以访问这个对象—让你可以方便地做这个检查:调用xhr?方法就可以了,这样,只要在add_to_cart中加上一行代码,不管用户的浏览器是否允许使用JS,我们的应用程序就都能支持了。
Controllers/store_controller.rb
def add_to_cart
begin
product = Product.find(params[:id])
rescue ActiveRecord::RecordNotFound
logger.error("Attempt to access invalid product #{params[:id]}")
redirect_to_index("Invalid product")
else
@cart = find_cart

@current_item = @car
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics