174: 用AJAX进行分页(观看原视频)
在任何一个要显示一系列项的Rails程序中,最好是能够把这一系列项进行分页,这样我们就可以一次只显示一定数量的项。已经有一些你可以用来做分页的gem,其中一个叫will_paginate的gem非常不错。我们下面的程序就使用了它。
下面显示的index页面来自于一个分页显示产品的程序。点击顶部每一页的链接将刷新整个页面,显示出产品列表中的不同部分。我们当前看到的产品页的页码显示在URL的query string当中。
这种分页方式使用标准的HTML链接,但是我们更想在点击这些链接时能够触发AJAX请求,这样我们就可以只更新页面中发生变化的部分。
分页
will_paginate gem可以从Github进行安装,把下面的代码加到/config/environment.rb中,就将其安装到了我们的程序里。
config.gem 'mislav-will_paginate', :lib => 'will_paginate', :source => 'http://gems.github.com'
在我们products controller的index action中我们使用paginate方法而不是find方法去读取我们的产品列表。
class ProductsController < ApplicationController
def index
@products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page])
end
end
paginate方法中值得注意的参数有两个,:per_page决定每次读取多少项;:page决定页码偏移值。这里,我们每页读取10项,页码取自于query string参数。
最后,在index view中我们用will_paginate来显示分页链接。将会显示“前一页”,“后一页”和每一页的链接。
<%= will_paginate @products %>
增加AJAX功能
为了让分页能够以AJAX的方式工作,我们需要修改分页的链接,使他们能够在点击时触发AJAX请求,而不是跳转到另外一页。要达到这样的目的,我们可以修改will_paginate的代码来改变生成链接的HTML代码,不过这样很麻烦,并且最终得到的是一个侵入式方案。一个更好的方法是,不修改原有的HTML,而是加上一些非侵入式的JavaScript代码来给这些链接加上AJAX功能。这种方法对于那些屏蔽了JavaScript的用户依然是可行的。
用jQuery程序库,我们可以很容易地实现AJAX功能。为此,我们需要下载jQuery,在我们程序目录 /public/javascripts下,将其保存为jquery.js文件。在程序的layout文件的<head>部分,我们加入下面一行代码来引用它。
<%= javascript_link_tag 'jquery' %>
如果你愿意,你也可以使用prototype而不是jQuery,但是你需要修改我们将要写的AJAX链接的代码,使其可以在prototype下工作。
我们将把用来做分页的JavaScript代码放到一个新的JavaScript文件pagination.js当中,和其他JavaScript文件放到同一目录下。首先我们要往这个文件中加一些页面中分页链接的onclick事件函数。但是我们想在文档对象模型(Document Object Model)加载以后再添加这些事件函数。通过调用$函数,传入一个函数作为参数,我们可以让浏览器在DOM加载完以后来执行这个函数。为了测试一下我们是不是已经正确配置,我们在DOM加载的时候显示一个alert。
$(function (){ alert('DOM has loaded.'); });
我们首先要把我们新的JavaScript文件包含在products页面中。之前在我们程序中用过Ryan Bates的很精巧的layout生成器,这里我们也可以用他提供的JavaScript辅助函数。在index view文件中,我们可以加入下面一行代码。
<% javascript 'pagination' %>
现在,当我们刷新页面的时候,pagination中的JavaScript将被加载,在页面加载时我们可以看到弹出的alert。
既然我们确信JavaScript已经能够被正确调用,我们就可以修改代码来让我们的分页链接使用AJAX。看看页面的代码,我们发现分页的链接都包含在class为pagination的一个div当中。我们可以利用这个来创建jQuery的选择器来匹配所有的链接。
<div class="pagination">
<a href="/products?page=1" class="prev_page" rel="prev start">« Previous</a>
<a href="/products?page=1" rel="prev start">1</a>
<span class="current">2</span>
<a href="/products?page=3" rel="next">3</a>
<!-- Other links omitted -->
<a href="/products?page=3" class="next_page" rel="next">Next »</a>
</div>
匹配到所有的链接后,我们就可以用click函数来改变他们的行为。我们修改后的分页代码如下所示。
$(function () { $('.pagination a').click(function () { $.get(this.href, null, null, 'script'); return false; }); });
当页面的DOM加载以后,我们的$函数依然会被调用,不再弹出一个alert,它现在做了一些其他事情。首先,它调用$函数,传入一个字符串来找到class为pagination的div中的所有锚标签。这些标签的click事件将被修改,当他们被点击时,就会发出AJAX请求。
这个AJAX请求是一个GET请求,所以我们可以使用jQuery的$.get方法。这个方法可以传入几个参数。第一个是请求的URL。我们想让我们的AJAX请求锚链接上同样的URL,所以我们可以用this.href来将href属性值传入函数。
第二个参数用来传入我们需要的任意数据。我们可以传入页码值,但是它已经放到URL的query string里面了,我们这里可以用null。同样,第三个参数是一个回调函数,我们不需要用它所以也可以传入null。
最后一个参数很重要。它告诉jQuery如果处理我们AJAX请求的返回内容。我们想要取回JavaScript代码让浏览器来执行,所以我们给这个参数传入’script’。
最后,函数返回false,这样的话链接的缺省行为就不会被触发,否则将会重新刷新页面,使得我们的AJAX请求变得毫无意义。
修改Controller
点击一个分页链接触发的AJAX请求会调用在没有加入JavaScript代码前同样的一个action:ProductController的index action。
def index
@products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page])
end
虽然这个action同样可以正确响应AJAX请求,但是与其关联的view文件返回的却是HTML。我们需要创建另外一个view以JavaScript的形式来返回一系列的产品项。为此,我们在目录/app/views/products下创建一个叫index.js.erb的文件。这个新view包含了分页链接被点击的时候返回的我们想让浏览器执行的JavaScript。
下面我们给这个view加一个简单的alert来测试我们的AJAX代码是不是可行。
alert("This is an AJAX request.");
如果你再次刷新页面,并且点击这些分页链接,你将看到弹出的alert。
现在浏览器运行的就是index action响应AJAX请求时返回的JavaScript。
毫无疑问,当分页链接被点击时,我们不只是想看到一个alert。我们想替换页面中显示链接和产品项的部分。为此,我们需要把链接和产品列表的部分抽取到一个partial当中。
index view的代码如下所示:
<% title "Products" %>
<% javascript 'pagination' %>
<%= will_paginate @products %>
<% @products.each do |product| %>
<div class="product">
<h3>
<%= link_to product.name, product %>
<%= number_to_currency(product.price, :unit => "£") %>
</h3>
<div class="actions">
<%= link_to "Edit", edit_product_path(product) %> |
<%= link_to "Destroy", product, :confirm => "Are you sure?", :method => :delete %>
</div>
</div>
<% end %>
<%= will_paginate @products %>
<p><%= link_to "New Product", new_product_path %></p>
我们要从view中抽取出来的是包含在两个有will_paginate的行之间的部分。我们把这一部分代码放到一个叫_product.html.erb的partial文件中,然后再在index中引用它。
<% title "Products" %>
<% javascript 'pagination' %>
<div id="products">
<%= render 'products' %>
</div>
<p><%= link_to "New Product", new_product_path %></p>
另外一个需要改动的地方是把partial放到一个div中,我们好在JavaScript中引用这个包含了内容的外层元素。也就是说,我们可以确切地知道我们要替换页面中的哪一部分。从Rails2.3开始,我们可以通过把一个partial的名字作为字符串传给render来显示它。
现在,我们回到刚才的index.js.erb文件,替换掉显示alert的代码,然后加入代码来更新那个包含了partial内容、id为products的div。所有这些只需要一行jQuery代码。
$("#products").html("<%= escape_javascript(render("products")) %>");
上面的代码从products partial中读取HTML,然后用escape_javascript来安全地将其插入到一个JavaScript字符串当中,然后将id为products的div中的内容替换成paritial的输出。
当我们现在刷新页面,并且点击“下一页”链接的时候,页面内容已经更新为下一页的产品项,但是页面的URL没有改变,但是当我们再次点击,页面又会全部刷新,URL也会改变。接下来的点击结果会在AJAX更新和整页刷新之间交替出现。
这是怎么回事?哦,原来是我们分页的JavaScript是在页面DOM加载完成的时候将分页链接绑定到click事件上的。当链接和内容被AJAX请求返回的内容替换掉时,事件绑定丢失了,所以链接行为又回到了原来的样子。我们点击的下一个链接刷新了整个页面,使得链接的click事件又被重新绑定,这些绑定的事件在另一个链接被点击、AJAX更新发生时又会失效。
幸亏,jQuery可以轻松搞定这个问题。不使用click方法将click事件绑定到链接上,我们可以利用live函数。这个函数多出的那一个参数是指定你要绑定的事件的名字,也就是click。
$(function () { $('.pagination a').live("click", function () { $.get(this.href, null, null, 'script'); return false; }); });
使用live(“click”, function() { … } 而不是click(),jQUery会使用事件代理来保证任何新的匹配的元素都将响应这个事件。如果我们现在刷新整个页面并且点击链接,每隔一次的页面刷新不见了,整个页面将始终是通过AJAX来更新的。
增加一个加载标示
结束本集之前,我们加一个标示,当用户点击链接时能够告诉他们页面正在加载。因为我们是在本机运行这个程序,所以响应极快,但是在真的环境中不一定会这样,所以我们需要给用户一个反馈。
为了在本地模拟一定的延时,我们在index action中sleep两秒钟。
def index
sleep 2
@products = Product.paginate(:all, :order => "name ASC", :per_page => 10, :page => params[:page])
end
如果我们现在点一个分页链接,页面更新的时候会有一个很短的停顿,但是没有任何地方显示说下一组数据正在被读取。
我们可以在我们pagination JavaScript中加这个标示。有很多方法可以来实现这个功能;其中最常见的方法是用一个转动的图片来显示操作正在进行中,这个图片通常是被隐藏的,必要时才显示出来。简单起见,当点了链接后,包含分页内容的pagination div将被一个消息串替代。
$(function () { $('.pagination a').live("click", function () { $('.pagination').html('Page is loading...'); $.get(this.href, null, null, 'script'); return false; }); });
现在,当我们点击一个分页链接,在读取的结果返回之前,分页链接部分会被一个“页面刷新中…”的消息串替换。
最后一点提示
如果你用的是更早版本的jQuery,你可能需要再加一行JavaScript代码使得AJAX请求能够在服务器端被正确处理。否则,可能返回HTML页面而不是JavaScript代码。如果你遇到这种情况,你需要添加下面的代码:
jQuery.ajaxSetUp({ 'beforeSend': function (xhr) {xhr.setRequestHeader('Accept', text/javascript')} });
本集内容就是这些。希望它能够让你很好地了解如何用jQuery来处理各种页面请求,不仅仅是分页,包括任何你想加到你的网站的各种AJAX功能。
下一集,我们将扩展本集的内容。如果你现在刷新页面,显示的产品列表会回到第一页,而不是刚才的地方。另外,我们没办法将一页收录到书签,或者用浏览器的返回按钮回到前一页。下一集将讨论这些内容并增强我们程序的可用性。