Rails入门2-创建文章(资源)并对文章进行CRUD操作

开始使用

前文已经介绍如何创建控制器、动作和视图,下面我们来创建一些更实质的功能。

在博客程序中,我们要创建一个新“资源”。资源是指一系列类似的对象,比如文章,人和动物。

资源可以被创建、读取、更新和删除,这些操作简称 CRUD。

Rails 提供了一个 resources 方法,可以声明一个符合 REST 架构的资源。创建文章资源后,config/routes.rb 文件的内容如下:

[root@ruby2 blog1]# vim config/routes.rb
Rails.application.routes.draw do
  get 'welcome/index'

  # The priority is based upon order of creation: first created -> highest priority.
  # See how all your routes lay out with "rake routes".

  # You can have the root of your site routed with "root"
  # root 'welcome#index'
  resources :articles  #创建资源
  root 'welcome#index'
  ...

执行 rake routes 任务,会看到定义了所有标准的 REST 动作。输出结果中各列的意义稍后会说明,现在只要留意 article 的单复数形式,这在 Rails 中有特殊的含义。

[root@ruby2 blog1]# ./bin/rake routes
       Prefix Verb   URI Pattern                  Controller#Action
welcome_index GET    /welcome/index(.:format)     welcome#index
     articles GET    /articles(.:format)          articles#index
              POST   /articles(.:format)          articles#create
  new_article GET    /articles/new(.:format)      articles#new
 edit_article GET    /articles/:id/edit(.:format) articles#edit
      article GET    /articles/:id(.:format)      articles#show
              PATCH  /articles/:id(.:format)      articles#update
              PUT    /articles/:id(.:format)      articles#update
              DELETE /articles/:id(.:format)      articles#destroy
         root GET    /                            welcome#index

下一节,我们会加入新建文章和查看文章的功能。这两个操作分别对应于 CRUD 的 C 和 R,即创建和读取。新建文章的表单如下所示:

new article

表单看起来很简陋,不过没关系,后文会加入更多的样式。

1.1 挖地基

首先,程序中要有个页面用来新建文章。一个比较好的选择是/articles/new。这个路由前面已经定义了,可以访问。打开 http://localhost:3000/articles/new ,会看到如下的路由错误:

new article

产生这个错误的原因是,没有定义用来处理该请求的控制器。解决这个问题的方法很简单:创建名为 ArticlesController 的控制器。执行下面的命令即可:

[root@ruby2 blog1]# ./bin/rails g controller articles
      create  app/controllers/articles_controller.rb
      invoke  erb
      create    app/views/articles
      invoke  test_unit
      create    test/controllers/articles_controller_test.rb
      invoke  helper
      create    app/helpers/articles_helper.rb
      invoke    test_unit
      create      test/helpers/articles_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/articles.js.coffee
      invoke    scss
      create      app/assets/stylesheets/articles.css.scss

打开刚生成的 app/controllers/articles_controller.rb 文件,会看到一个几乎没什么内容的控制器:

[root@ruby2 blog1]# cat app/controllers/articles_controller.rb 
class ArticlesController < ApplicationController
end

控制器就是一个类,继承自 ApplicationController。在这个类中定义的方法就是控制器的动作。动作的作用是处理文章的 CRUD 操作。

注:在 Ruby 中,方法分为 public、private 和 protected 三种,只有 public 方法才能作为控制器的动作。详情参阅 Programming Ruby 一书,也可以参考《ruby完全自学手册》的3.6.3章节。

现在刷新 http://localhost:3000/articles/new,会看到一个新错误:
new article

这个错误的意思是,在刚生成的 ArticlesController 控制器中找不到 new 动作。因为在生成控制器时,除非指定要哪些动作,否则不会生成,控制器是空的。

手动创建动作只需在控制器中定义一个新方法。打开 app/controllers/articles_controller.rb文件,在 ArticlesController 类中,定义 new 方法,如下所示:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def new
  end
end

在 ArticlesController 中定义 new 方法后,再刷新 http://localhost:3000/articles/new,看到的还是个错误:

new article

产生这个错误的原因是,Rails 希望这样的常规动作有对应的视图,用来显示内容。没有视图可用,Rails 就报错了。

这行信息还挺长,我们来看一下到底是什么意思。

第一部分说明找不到哪个模板,这里,丢失的是 articles/new 模板。Rails 首先会寻找这个模板,如果找不到,再找名为 application/new 的模板。之所以这么找,是因为 ArticlesController 继承自 ApplicationController。

后面一部分是个 Hash。:locale 表示要找哪国语言模板,默认
是英语(”en”)。:format 表示响应使用的模板格式,默认为 :html,所以 Rails 要寻找一个 HTML 模板。:handlers 表示用来处理模板的程序,HTML 模板一般使用 :erb,XML 模板使用 :builder,:coffee 用来把 CoffeeScript 转换成 JavaScript。

最后一部分说明 Rails 在哪里寻找模板。在这个简单的程序里,模板都存放在一个地方,复杂的程序可能存放在多个位置。

让这个程序正常运行,最简单的一种模板是 app/views/articles/new.html.erb。模板文件的扩展名是关键所在:第一个扩展名是模板的类型,第二个扩展名是模板的处理程序。Rails 会尝试在app/views 文件夹中寻找名为articles/new 的模板。这个模板的类型只能是 html,处理程序可以是 erb、builder 或 coffee。因为我们要编写一个 HTML 表单,所以使用 erb。所以这个模板文件应该命名为 articles/new.html.erb,还要放在 app/views 文件夹中。

新建文件 app/views/articles/new.html.erb,写入如下代码:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb

<h1>New Articles</h1>

再次刷新 http://localhost:3000/articles/new,可以看到页面中显示了一个标头。现在路由、控制器、动作和视图都能正常运行了。接下来要编写新建文章的表单了。

new article

1.2 首个表单

要在模板中编写表单,可以使用“表单构造器”。Rails 中常用的表单构造器是 form_for。在 app/views/articles/new.html.erb 文件中加入以下代码:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb

<h1>New Articles</h1>

<%= form_for :article do |f| %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

现在刷新页面,会看到上述代码生成的表单。在 Rails 中编写表单就是这么简单!

new article

调用 form_for 方法时,要指定一个对象。在上面的表单中,指定的是 :article。这个对象告诉 form_for,这个表单是用来处理哪个资源的。在 form_for 方法的块中,FormBuilder 对象(用 f 表示)创建了两个标签和两个文本字段,一个用于文章标题,一个用于文章内容。最后,在 f 对象上调用 submit 方法,创建一个提交按钮。

不过这个表单还有个问题。如果查看这个页面的源码,会发现表单 action 属性的值是 /articles/new。这就是问题所在,因为其指向的地址就是现在这个页面,而这个页面是用来显示新建文章表单的。

要想转到其他地址,就要使用其他的地址。这个问题可使用 form_for 方法的 :url 选项解决。在 Rails 中,用来处理新建资源表单提交数据的动作是 create,所以表单应该转向这个动作。

修改 app/views/articles/new.html.erb 文件中的 form_for,改成这样:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb

<h1>New Articles</h1>

<%= form_for :article, url: articles_path do |f| %>  #修改部分
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

这里,我们把 :url 选项的值设为 articles_path 帮助方法。要想知道这个方法有什么作用,我们要回过头再看一下 rake routes 的输出:

[root@ruby2 blog1]# ./bin/rake routes
       Prefix Verb   URI Pattern                  Controller#Action
welcome_index GET    /welcome/index(.:format)     welcome#index
     articles GET    /articles(.:format)          articles#index
              POST   /articles(.:format)          articles#create
  new_article GET    /articles/new(.:format)      articles#new
 edit_article GET    /articles/:id/edit(.:format) articles#edit
      article GET    /articles/:id(.:format)      articles#show
              PATCH  /articles/:id(.:format)      articles#update
              PUT    /articles/:id(.:format)      articles#update
              DELETE /articles/:id(.:format)      articles#destroy
         root GET    /                            welcome#index

articles_path 帮助方法告诉 Rails,对应的地址是 /articels,默认情况下,这个表单会向这个路由发起 POST 请求。这个路由对应于 ArticlesController 控制器的 create 动作。

表单写好了,路由也定义了,现在可以填写表单,然后点击提交按钮新建文章了。请实际操作一下。提交表单后,会看到一个熟悉的错误:

new article

解决这个错误,要在 ArticlesController 控制器中定义 create 动作。

1.3 创建文章

要解决前一节出现的错误,可以在 ArticlesController 类中定义 create 方法。在 app/controllers/articles_controller.rb 文件中 new 方法后面添加以下代码:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb 

class ArticlesController < ApplicationController
  def new
  end

  def create
  end
end

然后再次提交表单,会看到另一个熟悉的错误:找不到模板。现在暂且不管这个错误。create 动作的作用是把新文章保存到数据库中。

new article

提交表单后,其中的字段以参数的形式传递给 Rails。这些参数可以在控制器的动作中使用,完成指定的操作。要想查看这些参数的内容,可以把 create 动作改成:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb 

class ArticlesController < ApplicationController
  def new
  end

  def create
    render plain: params[:article].inspect
  end
end

render 方法接受一个简单的 Hash 为参数,这个 Hash 的键是 plain,对应的值为 params[:article].inspect。params 方法表示通过表单提交的参数,返回 ActiveSupport::HashWithIndifferentAccess 对象,可以使用字符串或者 Symbol 获取键对应的值。现在,我们只关注通过表单提交的参数。

如果现在再次提交表单,不会再看到找不到模板错误,而是会看到类似下面的文字:

new article

create 动作把表单提交的参数显示出来了。不过这么做没什么用,看到了参数又怎样,什么都没发生。

1.4 创建 Article 模型

在 Rails 中,模型的名字使用单数,对应的数据表名使用复数。Rails 提供了一个生成器用来创建模型,大多数 Rails 开发者创建模型时都会使用。创建模型,请在终端里执行下面的命令:

[root@ruby2 blog1]# ./bin/rails generate model Article title:string text:text
      invoke  active_record
      create    db/migrate/20141129012547_create_articles.rb
      create    app/models/article.rb
      invoke    test_unit
      create      test/models/article_test.rb
      create      test/fixtures/articles.yml

这个命令告知 Rails,我们要创建 Article 模型,以及一个字符串属性 title 和文本属性 text。这两个属性会自动添加到 articles 数据表中,映射到 Article 模型。

执行这个命令后,Rails 会生成一堆文件。现在我们只关注 app/models/article.rb 和 db/migrate/20141129012547_create_articles.rb(你得到的文件名可能有点不一样)这两个文件。后者用来创建数据库结构,下一节会详细说明。

注:Active Record 很智能,能自动把数据表中的字段映射到模型的属性上。所以无需在 Rails 的模型中声明属性,因为 Active Record 会自动映射。

1.5 运行迁移

如前文所述,rails generate model 命令会在 db/migrate 文件夹中生成一个数据库迁移文件。迁移是一个 Ruby 类,能简化创建和修改数据库结构的操作。Rails 使用 rake 任务运行迁移,修改数据库结构后还能撤销操作。迁移的文件名中有个时间戳,这样能保证迁移按照创建的时间顺序运行。

db/migrate/20141129012547_create_articles.rb(还记得吗,你的迁移文件名可能有点不一样)文件的内容如下所示:

[root@ruby2 blog1]# cat db/migrate/20141129012547_create_articles.rb 
class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.string :title
      t.text :text

      t.timestamps
    end
  end
end

在这个迁移中定义了一个名为 change 的方法,在运行迁移时执行。change 方法中定义的操作都是可逆的,Rails 知道如何撤销这次迁移操作。运行迁移后,会创建 articles 表,以及一个字符串字段和文本字段。同时还会创建两个时间戳字段,用来跟踪记录的创建时间和更新时间。

然后,使用 rake 命令运行迁移:

Rails 会执行迁移操作,告诉你创建了 articles 表。

[root@ruby2 blog1]# ./bin/rake db:migrate
== 20141129012547 CreateArticles: migrating ===================================
-- create_table(:articles)
   -> 0.0129s
== 20141129012547 CreateArticles: migrated (0.0133s) ==========================

注:因为默认情况下,程序运行在开发环境中,所以相关的操作应用于 config/database.yml 文件中 development 区域设置的数据库上。如果想在其他环境中运行迁移,必须在命令中指明:rake db:migrate RAILS_ENV=production

1.6 在控制器中保存数据

再回到 ArticlesController 控制器,我们要修改 create 动作,使用 Article 模型把数据保存到数据库中。打开 app/controllers/articles_controller.rb 文件,把 create 动作修改成这样:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def new
  end

  def create
    @article = Article.new(params[:article])

    @article.save
    redirect_to @article
  end
end

在 Rails 中,每个模型可以使用各自的属性初始化,自动映射到数据库字段上。create 动作中的第一行就是这个目的(还记得吗,params[:article] 就是我们要获取的属性)。@article.save 的作用是把模型保存到数据库中。保存完后转向 show 动作。稍后再编写 show 动作。

注:后文会看到,@article.save 返回一个布尔值,表示保存是否成功。

再次访问 http://localhost:3000/articles/new,填写表单,还差一步就能创建文章了,会看到一个错误页面:

new article

Rails 提供了很多安全防范措施保证程序的安全,你所看到的错误就是因为违反了其中一个措施。这个防范措施叫做“健壮参数”,我们要明确地告知 Rails 哪些参数可在控制器中使用。这里,我们想使用 title 和 text 参数。请把 create 动作修成成:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def new
  end
  def create
    @article = Article.new(article_params)

    @article.save
    redirect_to @article
  end

  private
    def article_params
      params.require(:article).permit(:title, :text)
    end
end

看到 permit 方法了吗?这个方法允许在动作中使用 title 和 text 属性。

注意:article_params 是私有方法。这种用法可以防止攻击者把修改后的属性传递给模型。关于健壮参数的更多介绍,请阅读这篇文章

1.7 显示文章

现在再次提交表单,Rails 会提示找不到 show 动作。这个提示没多大用,我们还是先添加 show 动作吧。
new article

我们在 rake routes 的输出中看到,show 动作的路由是:

article GET    /articles/:id(.:format)      articles#show

:id 的意思是,路由期望接收一个名为 id 的参数,在这个例子中,就是文章的 ID。

和前面一样,我们要在 app/controllers/articles_controller.rb 文件中添加 show 动作,以及相应的视图文件。

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def new
  end
  def create
    @article = Article.new(article_params)

    @article.save
    redirect_to @article
  end
  def show   #定义show动作
    @article = Article.find(params[:id])
  end
  private
    def article_params
      params.require(:article).permit(:title, :text)
    end

end

有几点要注意。我们调用 Article.find方法查找想查看的文章,传入的参数 params[:id] 会从请求中获取 :id 参数。我们还把文章对象存储在一个实例变量中(以 @ 开头的变量),只有这样,变量才能在视图中使用。

然后,新建 app/views/articles/show.html.erb 文件,写入下面的代码:

[root@ruby2 blog1]# vim app/views/articles/show.html.erb

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>
~                     

做了以上修改后,就能真正的新建文章了。访问 http://localhost:3000/articles/new,自己试试。

new article

5.8 列出所有文章

我们还要列出所有文章,对应的路由是:

articles GET    /articles(.:format)          articles#index

app/controllers/articles_controller.rb 文件中,为 ArticlesController 控制器添加 index 动作:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb 

class ArticlesController < ApplicationController
  def new
  end
  def create
    @article = Article.new(article_params)

    @article.save
    redirect_to @article
  end

  def show
    @article = Article.find(params[:id])
  end
  def index   #添加index动作
    @articles = Article.all
  end
  private
    def article_params
      params.require(:article).permit(:title, :text)
    end
end

然后编写这个动作的视图,保存为 app/views/articles/index.html.erb

[root@ruby2 blog1]# vim app/views/articles/index.html.erb

<h1>Listing articles</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Text</th>
  </tr>

  <% @articles.each do |article| %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.text %></td>
    </tr>
  <% end %>
</table>

现在访问http://localhost:3000/articles,会看到已经发布的文章列表。

new article

5.9 添加链接

至此,我们可以新建、显示、列出文章了。下面我们添加一些链接,指向这些页面。

打开 app/views/welcome/index.html.erb 文件,改成这样:

[root@ruby2 blog1]# vim app/views/welcome/index.html.erb 

<h1>Hello, Rails!</h1>
<%= link_to 'My Blog', controller: 'articles' %>

link_to 是 Rails 内置的视图帮助方法之一,根据提供的文本和地址创建超链接。这上面这段代码中,地址是文章列表页面。

new article

接下来添加到其他页面的链接。先在 app/views/articles/index.html.erb 中添加“New Article”链接,放在

标签之前:

[root@ruby2 blog1]# vim app/views/articles/index.html.erb 

<h1>Listing articles</h1>
<% link_to 'New article', new_article_path %>
<table>
  <tr>
    <th>Title</th>
    <th>Text</th>
  </tr>

  <% @articles.each do |article| %>
    <tr>
      <td><%= article.title %></td>
      <td><%= article.text %></td>
    </tr>
  <% end %>
</table>

点击这个链接后,会转向新建文章的表单页面。

然后在 app/views/articles/new.html.erb 中添加一个链接,位于表单下面,返回到 index 动作:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb 

<h1>New Articles</h1>
 <%= form_for :article, url: articles_path do |f| %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>
<%= form_for :article do |f| %>
  ...
<% end %>

<%= link_to 'Back', articles_path %>

new article

最后,在 app/views/articles/show.html.erb 模板中添加一个链接,返回 index 动作,这样用户查看某篇文章后就可以返回文章列表页面了:

[root@ruby2 blog1]# vim app/views/articles/show.html.erb 

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>
<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>
<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>

<%= link_to 'Back', articles_path %>

new article

注:如果要链接到同一个控制器中的动作,不用指定 :controller 选项,因为默认情况下使用的就是当前控制器。

注:在开发模式下(默认),每次请求 Rails 都会重新加载程序,因此修改之后无需重启服务器。

1.10 添加数据验证

模型文件,比如 app/models/article.rb,可以简单到只有这两行代码:

class Article < ActiveRecord::Base
end

文件中没有多少代码,不过请注意,Article 类继承自 ActiveRecord::Base。Active Record 提供了很多功能,包括:基本的数据库 CRUD 操作,数据验证,复杂的搜索功能,以及多个模型之间的关联。

Rails 为模型提供了很多方法,用来验证传入的数据。打开 app/models/article.rb文件,修改成:

[root@ruby2 blog1]# vim app/models/article.rb 

class Article < ActiveRecord::Base
validates :title, presence: true,
                    length: { minimum: 5 }

end

添加的这段代码可以确保每篇文章都有一个标题,而且至少有五个字符。在模型中可以验证数据是否满足多种条件,包括:字段是否存在、是否唯一,数据类型,以及关联对象是否存在。“Active Record 数据验证”一文会详细介绍数据验证。

添加数据验证后,如果把不满足验证条件的文章传递给 @article.save,会返回 false。打开 app/controllers/articles_controller.rb 文件,会发现,我们还没在 create 动作中检查 @article.save 的返回结果。如果保存失败,应该再次显示表单。为了实现这种功能,请打开 app/controllers/articles_controller.rb 文件,把 new 和 create 动作改成:

[root@ruby2 blog1]# vim app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render 'new'
    end
  end

  def show
    @article = Article.find(params[:id])
  end
  def index
    @articles = Article.all
  end
  private
    def article_params

在 new 动作中添加了一个实例变量 @article。稍后你会知道为什么要这么做。

注意,在 create 动作中,如果保存失败,调用的是 render 方法而不是 redirect_to 方法。用 render 方法才能在保存失败后把 @article 对象传给 new 动作的视图。渲染操作和表单提交在同一次请求中完成;而 redirect_to 会让浏览器发起一次新请求。

刷新 http://localhost:3000/articles/new,提交一个没有标题的文章,Rails 会退回这个页面,但这种处理方法没多少用,你要告诉用户哪儿出错了。为了实现这种功能,请在 app/views/articles/new.html.erb 文件中检测错误消息:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb 
<h1>New Articles</h1>
<%= form_for :article, url: articles_path do |f| %>
  <% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
      this article from being saved:</h2>
    <ul>
    <% @article.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

<%= link_to 'Back', articles_path %>

我们添加了很多代码,使用@article.errors.any?检查是否有错误,如果有错误,使用 @article.errors.full_messages 显示错误。

pluralize 是 Rails 提供的帮助方法,接受一个数字和字符串作为参数。如果数字比 1 大,字符串会被转换成复数形式。

在 new 动作中加入@article = Article.new 的原因是,如果不这么做,在视图中 @article 的值就是 nil,调用@article.errors.any? 时会发生错误。

注:Rails 会自动把出错的表单字段包含在一个 div 中,并为其添加了一个 class:field_with_errors。我们可以定义一些样式,凸显出错的字段。

再次访问 http://localhost:3000/articles/new,尝试发布一篇没有标题的文章,会看到一个很有用的错误提示。

new article

1.11 更新文章

我们已经说明了 CRUD 中的 CR 两种操作。下面进入 U 部分,更新文章。

首先,要在 ArticlesController 中添加 edit 动作:

[root@ruby2 blog1]# cat app/controllers/articles_controller.rb 
class ArticlesController < ApplicationController
  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render 'new'
    end
  end 

  def show
    @article = Article.find(params[:id])
  end
  def index
    @articles = Article.all
  end
  def edit  #添加edit动作
    @article = Article.find(params[:id])
  end
  private
    def article_params
      params.require(:article).permit(:title, :text)
    end  
end

视图中要添加一个类似新建文章的表单。新建 app/views/articles/edit.html.erb 文件,写入下面的代码:

[root@ruby2 blog1]# vim app/views/articles/edit.html.erb

<h1>Editing article</h1>

<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
  <% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
      this article from being saved:</h2>
    <ul>
    <% @article.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

<%= link_to 'Back', articles_path %>

这里的表单指向 update 动作,现在还没定义,稍后会添加。

method::patch 选项告诉 Rails,提交这个表单时使用 PATCH 方法发送请求。根据 REST 架构,更新资源时要使用 HTTP PATCH 方法。

form_for 的第一个参数可以是对象,例如 @article,把对象中的字段填入表单。如果传入一个和实例变量(@article)同名的 Symbol(:article),效果也是一样。上面的代码使用的就是 Symbol。详情参见 form_for 的文档

然后,要在 app/controllers/articles_controller.rb 中添加 update 动作:

[root@ruby2 blog1]# cat app/controllers/articles_controller.rb 
class ArticlesController < ApplicationController
  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render 'new'
    end
  end 

  def show
    @article = Article.find(params[:id])
  end
  def index
    @articles = Article.all
  end
  def edit
    @article = Article.find(params[:id])
  end
  def update #定义update动作
  @article = Article.find(params[:id])

  if @article.update(article_params)
    redirect_to @article
  else
    render 'edit'
  end
end
  private
    def article_params
      params.require(:article).permit(:title, :text)
    end  
end

新定义的 update 方法用来处理对现有文章的更新操作,接收一个 Hash,包含想要修改的属性。和之前一样,如果更新文章出错了,要再次显示表单。

上面的代码再次使用了前面为 create 动作定义的 article_params 方法。

注:不用把所有的属性都提供给 update 动作。例如,如果使用 @article.update(title: 'A new title'),Rails 只会更新 title 属性,不修改其他属性。

最后,我们想在文章列表页面,在每篇文章后面都加上一个链接,指向 edit 动作。打开 app/views/articles/index.html.erb 文件,在“Show”链接后面添加“Edit”链接:

[root@ruby2 blog1]# vim app/views/articles/index.html.erb 

<h1>Listing articles</h1>
<% link_to 'New article', new_article_path %>
<table>
  <tr>
    <th>Title</th>
    <th>Text</th>
    <th colspan="2"></th>
  </tr>

<% @articles.each do |article| %>
  <tr>
    <td><%= article.title %></td>
    <td><%= article.text %></td>
    <td><%= link_to 'Show', article_path(article) %></td>
    <td><%= link_to 'Edit', edit_article_path(article) %></td>
  </tr>
<% end %>
</table>

我们还要在 app/views/articles/show.html.erb 模板的底部加上“Edit”链接:

[root@ruby2 blog1]# vim app/views/articles/show.html.erb 

<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>
<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>
<p>
  <strong>Title:</strong>
  <%= @article.title %>
</p>

<p>
  <strong>Text:</strong>
  <%= @article.text %>
</p>

<%= link_to 'Back', articles_path %>
<%= link_to 'Edit', edit_article_path(@article) %>

new article

1.12 使用局部视图去掉视图中的重复代码

编辑文章页面和新建文章页面很相似,显示表单的代码是相同的。下面使用局部视图去掉两个视图中的重复代码。按照约定,局部视图的文件名以下划线开头。

注:关于局部视图的详细介绍参阅“Layouts and Rendering in Rails”一文。

新建 app/views/articles/_form.html.erb 文件,写入以下代码:

[root@ruby2 blog1]# vim app/views/articles/_form.html.erb

<%= form_for @article do |f| %>
  <% if @article.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@article.errors.count, "error") %> prohibited
      this article from being saved:</h2>
    <ul>
    <% @article.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
  <% end %>
  <p>
    <%= f.label :title %><br>
    <%= f.text_field :title %>
  </p>

  <p>
    <%= f.label :text %><br>
    <%= f.text_area :text %>
  </p>

  <p>
    <%= f.submit %>
  </p>
<% end %>

除了第一行 form_for 的用法变了之外,其他代码都和之前一样。之所以能在两个动作中共用一个 form_for,是因为 @article 是一个资源,对应于符合 REST 架构的路由,Rails 能自动分辨使用哪个地址和请求方法。

关于这种 form_for 用法的详细说明,请查阅 API 文档

下面来修改 app/views/articles/new.html.erb 视图,使用新建的局部视图,把其中的代码全删掉,替换成:

[root@ruby2 blog1]# vim app/views/articles/new.html.erb 
<h1>New article</h1>

<%= render 'form' %>

<%= link_to 'Back', articles_path %>

然后按照同样地方法修改app/views/articles/edit.html.erb 视图:

[root@ruby2 blog1]# vim app/views/articles/edit.html.erb

<h1>Edit article</h1>

<%= render 'form' %>

<%= link_to 'Back', articles_path %>

1.13 删除文章

现在介绍 CRUD 中的 D,从数据库中删除文章。按照 REST 架构的约定,删除文章的路由是:

DELETE /articles/:id(.:format)      articles#destroy

删除资源时使用 DELETE 请求。如果还使用 GET 请求,可以构建如下所示的恶意地址:

<a href='http://example.com/articles/1/destroy'>look at this cat!</a>

删除资源使用 DELETE 方法,路由会把请求发往 app/controllers/articles_controller.rb 中的 destroy 动作。destroy 动作现在还不存在,下面来添加:

[root@ruby2 blog1]# cat app/controllers/articles_controller.rb 
class ArticlesController < ApplicationController
  def new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)

    if @article.save
      redirect_to @article
    else
      render 'new'
    end
  end 

  def show
    @article = Article.find(params[:id])
  end
  def index
    @articles = Article.all
  end
  def edit
    @article = Article.find(params[:id])
  end
  def update
  @article = Article.find(params[:id])
  def destroy  #定义删除动作
    @article = Article.find(params[:id])
    @article.destroy

    redirect_to articles_path
  end

  if @article.update(article_params)
    redirect_to @article
  else
    render 'edit'
  end
end
  private
    def article_params
      params.require(:article).permit(:title, :text)
    end  
end

想把记录从数据库删除,可以在 Active Record 对象上调用 destroy 方法。注意,我们无需为这个动作编写视图,因为它会转向 index 动作。

最后,在 index 动作的模板(app/views/articles/index.html.erb)中加上“Destroy”链接:

[root@ruby2 blog1]# vim app/views/articles/index.html.erb

<h1>Listing Articles</h1>
<%= link_to 'New article', new_article_path %>
<table>
  <tr>
    <th>Title</th>
    <th>Text</th>
    <th colspan="3"></th>
  </tr>

<% @articles.each do |article| %>
  <tr>
    <td><%= article.title %></td>
    <td><%= article.text %></td>
    <td><%= link_to 'Show', article_path(article) %></td>
    <td><%= link_to 'Edit', edit_article_path(article) %></td>
    <td><%= link_to 'Destroy', article_path(article),
                    method: :delete, data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

生成“Destroy”链接的 link_to 用法有点不一样,第二个参数是具名路由,随后还传入了几个参数。:method:'data-confirm' 选项设置链接的 HTML5 属性,点击链接后,首先会显示一个对话框,然后发起 DELETE 请求。这两个操作通过 jquery_ujs 这个 JavaScript 脚本实现。生成程序骨架时,会自动把 jquery_ujs 加入程序的布局中(app/views/layouts/application.html.erb)。没有这个脚本,就不会显示确认对话框。

new article

恭喜,现在你可以新建、显示、列出、更新、删除文章了。

注:一般情况下,Rails 建议使用资源对象,而不手动设置路由。关于路由的详细介绍参阅“Rails 路由全解”一文。

参考文献: http://guides.ruby-china.org/getting_started.html


交流方式:

微信公众号:puppet2014,可微信搜索加入,也可以扫描以下二维码进行加入

微信公众号

QQ交流群:296934942