ict.dtd"> RJS教程

第四章

RJS 实践: The Expense Tracker

迄今为止我们完成了 "Thought Log" 应用,也初步了解了 RJS 是如何融入 Rails 框架中的。现在我们再做一个例子,这个例子更贴近真实并且会解决很多你在自己项目中可能会遇到的问题。为了不让我们的开发支出脱离我们的掌控,所以现在就来创建一个简单的应用帮助我们来追踪这些。

创建 Models

首先,我们用 Rails 的模型生成器来创建我们项目要用到的模型。Rails 的模型生成器会自动创建模型和相关的迁移文件。我们只需要在其中增加我们需要的功能就可以。

expenses> ruby script/generate model Project
     exists app/models/
     exists test/unit/
     exists test/fixtures/
     create app/models/project.rb
     create test/unit/project_test.rb
     create test/fixtures/projects.yml
     create db/migrate
     create db/migrate/001_create_projects.rb

expenses> ruby script/generate model Expense
     exists app/models/
     exists test/unit/
     exists test/fixtures/
     create app/models/expense.rb
     create test/unit/expense_test.rb
     create test/fixtures/expenses.yml
     exists db/migrate
     create db/migrate/002_create_expenses.rb

发生器创建了 app/models/project.rbapp/models/expense.rb 两个文件,并分别在其中定义了 Project 和 Expense 模型,包括单元测试和测试夹具。发生器也帮我们创建了迁移文件 db/migrate/001_create_projects.rbdb/migrate/002_create_expenses.rb

现在模型生成器为我们创建了两个新的迁移文件,我们要在其中增加需要用到的字段定义。打开 db/migrate/001_create_projects.rb 像下面代码一样编辑:

class CreateProjects < ActiveRecord::Migration
 def self.up
  create_table :projects do |t|
   t.column :name, :string
  end
 end

 def self.down
  drop_table :projects
 end
end

我们在迁移中增加一行,t.column :name:string。这行代码在 projects 表中增加了名字是 name 字段类型是 String 的字段。下一步,为表 expenses 定义字段。同理,打开 db/migrate/002_create_expenses.rb 文件然后增加 project_iddescriptionamount 的字段定义。

class CreateExpenses < ActiveRecord::Migration
 def self.up
  create_table :expenses do |t|
   t.column :project_id, :integer
   t.column :description, :string
   t.column :amount, :float
  end
 end

 def self.down
  drop_table :expenses
 end
end

当数据库创建完毕并且连接配置成功,我们可以运行迁移文件。它将根据迁移文件中的字段定义在 config/database.yml 中定义的开发数据库内创建两个表。

expenses> rake migrate

现在 Expense Tracker 需要的数据库部分都搞定了,我们可以定义模型之间的关系。 一个 Project 对应许多 Expense 对象,所以在文件 app/models/project.rb 中的 Project 模型里增加 has_many() 关系。

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

我们在 has_many() 后调用了 :dependent => :delete_all 参数,因为我们数据中不希望存在一个 expenses 而他却不属于任何一个 Project。打开 app/models/expense.rb。现在,在 Expense 模型中定义 belongs_to() 关系。一个 Expense 对象 belongs_to() 一个 Project。因为 Expense 包含外键。

class Expense < ActiveRecord::Base
 belongs_to :project
end

现在模型和数据库都定义好了,我们可以进行下一步开发控制器部分。

定义 Controllers

先生成两个控制器。第一个为了 projects,滴二个为了 expenses。

expenses> ruby script/generate controller Projects
     exists app/controllers/
     exists app/helpers/
     create app/views/projects
     exists test/functional/
     create app/controllers/projects_controller.rb
     create test/functional/projects_controller_test.rb
     create app/helpers/projects_helper.rb

expenses> ruby script/generate controller Expenses
     exists app/controllers/
     exists app/helpers/
     create app/views/expenses
     exists test/functional/
     create app/controllers/expenses_controller.rb
     create test/functional/expenses_controller_test.rb
     create app/helpers/expenses_helper.rb

再一次,使用发生器简单的就为 project 创建出了所需的文件,包括模板视图的文件夹和控制器相关的测试。

show() 动作根据 params Hash 中传递回来的 :id 参数来找到 Project 对象。编辑 app/controllers/projects_controller.rb 文件,然后增加如下代码。

class ProjectsController < ApplicationController
 def show
  @project = Project.find(params[:id])
 end
end

接下来,编辑 app/controllers/expenses_controller.rb 增加创建 Expense 对象的代码。

class ExpensesController < ApplicationController
 before_filter :find_project

 def new
  @expense = @project.expenses.create(params[:expense])
 end

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

ExpensesControllerProjectsController稍微复杂一点。既然每一个 Expense 对象都属于一个 Project,我们可以用 before_filter 保存很多相同的代码。before_filter 在每一个控制器的动作执行之前执行。我们定义的过滤器,自动根据 params Hash 中的 :project 键找到 Project,并将其储藏在变量 @project 中。

new 动作,就跟其名字所表达的意义一样,给一个 Project 增加一个 Expense 对象。Rails 在 RJS 模板方面遵循和 RHTML 和 RXML 模板一样的约定。Rails 根据控制器的动作名称去寻找同名的模板文件。那么,当触发 new 动作时,控制器自动寻找 app/views/expenses/new.rjs 模板。

设置路由

接下来我们设置 before_filter 和创建更漂亮的 URLs。controller 根据 params[:project] 传递的值来寻找 project。我们需要增加一个简单的路由规则来产生类似 /projects/1/expenses/new 的漂亮的 url 。

打开 config/routes.rb 在默认的路由设置之下增加如下代码:

map.expenses 'projects/:project/expenses/:action/:id', :controller => 'expenses'

在开发模式下,路由的改变会立即生效。继续,我们给应用创建一个布局模板。

创建一个应用布局

我们需要一个布局来包含我们的内容。ActionController::Base 后代自动的根据控制器的类名寻找一个布局模板。这意味着 ApplicationController 会自动寻找一个名为 application.rhtml 的布局模板。我们的控制器都是 ApplicationController 的后代,所以会很好的继承下来,除非隐藏。创建 app/views/layouts/application.rhtml 并在编辑器中增加如下代码:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <title>Expense Tracker</title>
  <%= stylesheet_link_tag "screen.css" %>
  <%= javascript_include_tag :defaults %>
 </head>
 <body>
  <div id="content">
   <%= yield %>
  </div>
 </body>
</html>

注意,我们使用 Rails 辅助方法 stylesheet_link_tag() 来包含样式表 screen.css,即 public/stylesheets/screen.css。我使用了一些简单的风格,代码如下:

th { text-align: left; }
#content { margin: 10px; }
#content p { margin-bottom: 10px; }
#expenses,#summary { border: none; border-collapse: collapse; width: 600px;}
.amount { width: 40%; }
.amount, .total { text-align: right; }
#new-expense { margin-top: 2em; background-color: #eeede5; padding: 1em; }
#new-expense h3 { margin-top: 0.5em; }
#total { border: none; border-collapse: collapse; width: 600px;}
#total-amount, .total { font-weight: bold; background-color: #eeede5; }
#total-amount { border-top: 2px solid black; }
.total { width: 90%; padding-right: 10px; }

插入数据

我们需要一些数据来显示我们的页面,所以我要输入一些数据。运行控制台。

expenses> ruby script/console
>> rjs_book = Project.create(:name => 'RJS Templates for Rails')
=> #<Project:0xb72e444c8 ...>
>> rjs_book.expenses.create(:description => 'Americano at Bridgehead', :amount => 1.93)
=> #<Expense:0xb72cb84c ...>
>> rjs_book.expenses.create(:description => 'Sandwich at La Bottega', :amount => 4.27)
=> #<Expense:0xb72c3b24 ...>
>> quit

我不认为我侥幸逃脱了费用支出,但是至少我的钱花到了更合适的地方。现在这些样例数据需要一些视图。

创建视图

我们还没有为我们的 Project 视图而进行任何开支。我们需要创建一个视图来展示 Expense 对象,例如刚创建的 Project 。视图分为一个模板和两个局部模板。我们明确分离 _expense.rhtml 局部,所以当用 RJS 更新表格时,我们可以渲染一个单独行。

创建视图文件 app/views/projects/show.rhtml 并增加如下代码:

<h1><%= @project.name %></h1>

<h2>Expenses</h2>
<table id="expenses">
 <tr><th>Description</th><th class="amount">Amount</th></tr>
 <%= render :partial => 'expenses/expense', :collection => @project.expenses %>
</table>

<%= render :partial => 'expenses/new' %>

这个局部模板给了表格一个 id expenses,因此我们可以在更新页面时查找到它。我们给局部模板一个相对路径,因为我们从 ProjectsController 渲染 show.rhtml,但是局部模板在 app/views/expenses 视图文件夹中。

现在我们创建局部模板 app/views/expenses/_expense.rhtml。这个局部模板在 <table> 内部渲染实际的 Expense 模板。

<tr id="expense-<%= expense.id %>">
 <td><%=h expense.description %></td>
 <td class="amount"><%=h number_with_precision(expense.amount, 2) %></td>
</tr>

注意,我们是如何用 Expense 对象的 id 属性来设置每一行的 id。我们这么做的原因和我们给 <table> 一个 id: 一样,它允许我们更新每一行。number_with_precision() 是一个内置的 Rails 辅助方法用来根据参数中来显示小数位数。我们也使用 h() 方法,避免 HTML 破坏页面。转义 HTML 防止用户在 project 的 title 中提交 JavaScript 等恶意的代码。

最后,我们增加局部模板到表单中。将如下代码放入 app/views/expenses/_new.rhtml 中:

<div id="new-expense">
 <h3>Add an expense</h3>
 <% form_remote_for :expense, Expense.new, :url => hash_for_expenses_url(:project => @project, :action => 'new'),
          :html => { :id => 'expense-form' } do |f| %>
  <label for="expense_description">Description:</label><br />
  <%= f.text_field 'description', :size => 60 %><br />

  <label for="expense_amount">Amount:</label><br />
  <%= f.text_field 'amount', :size => 10 %><br /><br />

  <%= submit_tag 'Add Expense' %>
 <% end %>
</div>

这不是一个规则的表单。我们使用 form_remote_for() 方法,它用 Ajax 请求在后台提交数据到控制器的动作。第一个参数传递对象的名字;表单数据储存在 params Hash 中这个键值下。第二个参数是对象,提供给表单的初始数据。然后我们传递 :url 选项,它告诉表单将数据提交到哪里。注意,我们调用路由辅助方法 hash_for_expenses_url(),它产生根据 expensesconfig/routes.rb:url Hash 的参数用来调用这个方法。我们也给 form 一个 id 名为 expense-form 因此 RJS 和 JavaScript 代码可以引用 from 。

最后,我们创建一个 RJS 模板当我们增加一个新的 expense 时更新我们的 project 的 expense 页面。创建 app/views/expenses/new.rjs 并增加如下代码:

page.insert_html :bottom, 'expenses', :partial => 'expense'
page.visual_effect :highlight, "expense-#{@expense.id}"
page.form.reset 'expense-form'

第一行代码用局部模板 _expense.html 生成的 HTML 插入 id expenses DOM 元素的下面。选项 :bottom 指定局部模板生成的 HTML 将插入元素现有内容的下面。因为有一个实例变量 @expense 并且局部模板名为 expense,@expense 自动的在局部模板内部使用局部变量 expense 来替换。因此我们通过选项 :object => @expense 传递。

第二行代码请求一个 Scriptaculous 视觉效果加亮一个新的 Expense 对象。最后,第三行代码用 RJS 类来复位表单。接下来,我们做一个测试驱动开开发生了什么。

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

Ů Ż Ϣ ƷQQ UFO ɽŻ ַز ŷʱ ̷ װ Ż ݵһŻ Խ Ʊ ֶ Ӣѧϰa Ż ذ Ϻ֮ ˫ﲻý 人Ż QQ Цȫ ʯׯ õտ ɽŻ QQռ ̫ԭ ֣ݰ С˵ ³ľŻ ɳ ܽ Żվ վ Ż Ϸ һŻ