第六章

增强 Expense Tracker

Expense Tracker 现在使用 Ajax 在后台调用来给 Project 增加一个 Expense 对象。虽然表单工作正常并且成功的为 project 增加了 expenses ,它还可以做的更好。接下来我们将要给页面增加一个行为指示器,并且稍后,我们增加一个概要章节来展一些关于 project 的统计表。

Ajax 行为指示器

Ajax 的一个问题是它违反了用户习惯的浏览习惯。当用户执行一个与服务器交互的动作后,更习惯看到重新加载整个页面。没有任何的指示表明页面是忙碌的会让用户觉得很奇怪。用户或许觉得页面没有反应并重新点按钮,结果就是想象之外的效果了。

解决这个问题的方法之一是在页面上给出一些提示,让用户知道正在发生一个远程调用。这个例子中,我们使用动态的 GIF ,并附带一些描述行的文字。我们也可以禁用表单以避免当表单提交数据的时候,用户再次点击提交按钮而再次提交数据。

我们可以当 Ajax 回调的时候,利用 JavaScript 完成这些功能。 Rails 允许你在远程调用的过程中加入一些选项来参与回调。可能用到的回调包括: :uninitialized, :loading, :loaded, :interactive, :complete, :failure:success。可以看看 Rails 文档仔细了解关于这些的详细信息。

创建 public/javascripts/application.js 然后增加如下代码:

var ExpenseTracker = {}

ExpenseTracker = {
 disableExpenseForm: function() {
  Element.show('form-indicator');
  Form.disable('expense-form');
 },

 enableExpenseForm: function(form) {
  Element.hide('form-indicator');
  Form.enable('expense-form');
 }
}

我们创建了一个新的 JavaScript 对象 ExpenseTracker,我们的页面会用到它。然后我们增加一个简单的方法,disableExpenseForm(),用来显示一个自转的指示器并在后台发生 Ajax 请求时禁用表单。我们也增加了 enableExpenseForm(),用来隐藏指示器并在调用完成后启用表单。我们可以立即调用这些方法而不需要 ExpenseTracker 对象,但是我们将来需要为每个方法增加更多的功能并把这些功能压缩到一起。事实上我也喜欢用代码来管理表单这样不会让 RJS 模板变散乱。

因为我们使用 javascript_include_tag :defaults 在我们的布局模板中,Rails 巧妙的包含了 public/javascripts/application.js 连同 Rails JavaScript 库。现在我们那个简单的 JavaScript 函数已经准备好了,我们可以在回调发生的时候引发这个钩子函数。展开 app/views/expenses/_new.rhtml 并修改它使其包含回调。 form_remote_for() 方法调用的完整代码如下:

<% form_remote_for :expense,
         Expense.new,
         :url => hash_for_expenses_url(:project => @project, :action => 'new'),
         :loading => 'ExpenseTracker.disableExpenseForm()',
         :complete => 'ExpenseTracker.enableExpenseForm()',
         :html => { :id => 'expense-form' } do |f| %>

表单有 id expense-form 所以我们可以用 JavaScript 函数来更新它。我们在 RJS 模板中用 enableExpenseForm() 方法也能达到同样的效果,但是它能更好的维持回调代码在 ExpenseTracker 对象中。这也是在 RJS 模板中管理表单的技巧。我们也能在 enableExpenseForm() 方法中重置表单,但这会让表单一直重置。我们可能希望控制表单何时重置,因此用户在验证失败或发生其他问题是不会再提交表单数据。

接下来,在 submit_tag() 访问 app/views/expenses/_new.rhtml 后增加指示器图片。设置初始样式 display:none ,这样当页面第一次载入的时候指示器不会被显示出来。我的指示器只是一个简单的图片模拟 Mozilla Firefox's 自转指示器。我将图片放置在项目中的 public/images 文件夹。

<%= image_tag 'indicator.gif', :id => 'form-indicator', :style => 'display:none;' %>

现在当提交表单的时候,浏览器会显示一个自转的图片并禁用表单。这样形象的告诉了用户,正有一个 Ajax 调用在进行中用户也无法在这时重复提交表单。当调用完成后,指示器隐藏起来并且表单不再禁用。在这个例子中,整个过程的发生其实很快以至于你可能还没有意识到指示器的显示和表单的禁用这个过程。你可以在 ExpenseControllernew() action 中增加一个 sleep() 来延缓这个过程。显然你只是在开发模式下用来测试这个过程。sleep() 代码如下:

class ExpensesController < ApplicationController
 before_filter :find_project

 def new
  @expense = @project.expenses.create(params[:expense])
  # Sleep for 3 seconds
  sleep 3
 end

 private
 def find_project
  @project = Project.find(params[:project])
 end
end

Ajax 全局应答

form_remote_tag() 的 :loading:complete 回调可以很好的显示和隐藏表单指示器。这里有个问题就是如果在页面中有很多的 Ajax 功能,为每一个远程操作增加 :loading:complete 显示和隐藏图片指示器的过程是很单调的。这样 Ajax 全局应答就用得上了。

Prototype 公共 Ajax 响应是一个给每个 Ajax 响应放置 JavaScript 函数的好地方。Ajax 公共调用是由 Prototype 库提供的;他们允许你为所有的 Ajax 调用采用多种多样的钩子函数。让我们移除用 :loading:complete 回调来显示和隐藏指示器的代码,而用 Ajax 公共响应来代替。

代替每一个表单旁的图片指示器,Ajax 公共响应允许你在页面上设置一个单一的指示器,他会在每一个 Ajax 响应的期间显示。

设置一个 Ajax 公共响应是很简单的。你可以增加如下代码到 public/javascripts/application.js 中:

Ajax.Responders.register({
 onCreate: function() {
  if (Ajax.activeRequestCount > 0)
  Element.show('form-indicator');
 },
 onComplete: function() {
  if (Ajax.activeRequestCount == 0)
   Element.hide('form-indicator');
 }
});

代码很简单。Ajax.Responders.register() 获取一个匿名的 JavaScript 对象,它的属性名是一个 Ajax 回调,值是一个 JavaScript 函数。我们定义的一个函数会在每一个 onCreate() 回调中执行,另一个函数会在每个 onComplete() 回调过程中执行。第一个函数当有一个或多个活动的 Ajax 请求是显示 id 是 form-indicator 的 DOM 元素。第二个函数当没有 Ajax 活动的时候隐藏指示器。

现在我们可以移除 public/javascripts/application.js 中显示和隐藏指示器图片的 ExpenseTracker 对象,或者我们可以完全移除 ExpenseTracker JavaScript 代码并只在回调内部写入代码。如果我们消除了 ExpenseTracker JavaScript 对象并简单的在 app/views/expenses/_new.rhtml 代码内部嵌入 form_remote_for

<% form_remote_for :expense,
         Expense.new,
         :url => hash_for_expenses_url(:project => @project, :action => 'new'),
         :loading => 'Form.disable("expense-form")',
         :complete => 'Form.enable("expense-form")',
         :html => { :id => 'expense-form' } do |f| %>

现在回调可以启用和禁用表单。该代码用来显示和隐藏的指示器,由 Ajax 公共响应来实行。每当有 Ajax 活动的时候指示器显示,而当 Ajax 调用完成后,指示器隐藏起来。我们可以增加更多的功能,例如把指示器图片移到屏幕的一角,或使用一个活动的高亮图片。现在,我们只把图片放到同一个地方。

Ajax 公共方法提供了一个非常好的在每一个 AJAX 的请求的生命周期执行动作的方法。这不仅减少了代码的重复,这使得我们的模板,更容易理解的,而且,也节省了大量的打字。

Model 验证

在当前的状态下,Expense Tracker 会接受任何形式的输入并试图创建 Expense 对象。问题是应用阻塞输入验证。最有可能的情况是 ActiveRecord 抛出一个异常,它不是我们的代码。我们增加的 Ajax 指示器还在那里自转而用户不知道发生了什么。很幸运,Rails 有不错的 model 验证。我么可以验证新的 Expense 对象并用一个漂亮的提示框包含给用户的信息。让我们为 app/models/expense.rb 中的 Expense model 增加一些验证吧:

class Expense < ActiveRecord::Base
 belongs_to :project

 validates_presence_of :description
 validates_numericality_of :amount

 protected
 def validate
  errors.add(:amount, "must be greater than 0") unless amount.nil? || amount >= 0.01
 end
end

这个验证代码保证 description 字段不为空,Expense 十一个数字对象并且大于0。现在我们稍微修改一下我们的 RJS 模板来显示错误信息。打开 app/views/expenses/new.rjs 然后按下面的代码修改:

if @expense.new_record?
 page.alert "The Expense could not be added for the following reasons:\n" +
      @expense.errors.full_messages.join("\n")
else
 page.insert_html :bottom, 'expenses', :partial => 'expense'
 page.visual_effect :highlight, "expense-#{@expense.id}"
 page.form.reset 'expense-form'
end

这个代码验证 Expense 对象是否仍是新记录。如果它是一个新对象,那肯定是在储存的时候有一些问题并显示错误信息。否则,一个普通的插入动作会执行插入一个新 Expense。注意仅当操作成功时表单重置。这样,当出错的时候用户不用再次重复输入 descriptionamount

在本例中,我们只用了一个简单的JavaScript警告框,以显示错误。这是一个在 RJS 中显示错误的简单方法。另一种解决办法,将取代整个表格,并用 error_messages_for() 在页面上输出错误信息。这充分利用的 Rails 内置的辅助方法,但是也可需要在 RJS 模板中做更多的修改,因为你将不得不在 Expense 对象成功添加后删除或隐藏错误信息。

增加一些考虑

如果当请求发生的过程中 Expense 表单禁用,无疑会提高可用性。但是有很长的路要走。我还不知道 project 的 Expenses 总数是多少。同样,我们添加一些代码来显示其它需要注意的数据,比如:Expense 的最小值,Expense 的最大值,以及 project 中 expense 的平均值。我们将必须确保在给 project 增加 expenses 时,所有被添加的信息都得到更新。

首先,要增加一些考虑方法在 Project 模型中。打开 app/models/project.rb 增加 calculation 方法。你的模型开起来像下面这样:

class Project < ActiveRecord::Base
 has_many :expenses, :dependent => :delete_all

 def total_expenses
  expenses.sum(:amount)
 end

 def min_expense
  expenses.minimum(:amount)
 end

 def max_expense
  expenses.maximum(:amount)
 end

 def avg_expense
  expenses.average(:amount)
 end
end

These methods are all ridiculously simple. We use the power of the new Active Record Calculations (added in Rails 1.1) to do all of the dirty work. Notice that the calculation methods are being called from the expenses collection. Calling each calculation from the collection instead of from the Expense class causes the calculation to be scoped to the current Project, which is what we want in this case. We pass in the Symbol :amount to each calculation because that is the Expense attribute on which we want to perform the calculation.

We might as well display all of this information on the page that shows the project's expenses. We can show the total expenses using a partial that we'll render directly under the list of expenses. Create app/views/expenses/_total.rhtml, which will look like this:

<table id="total">
 <tr>
  <td></td>
  <td class="total">Total</td>
  <td id="total-amount" class="amount"><%= number_to_currency(total) %></td>
 </tr>
</table>

© Railser.cn 里克的网络自习室,仅供学习参考,更新于2008年3月23日