在线版的内容可能落后于电子书,如果想及时获得更新,请购买电子书

第 2 章 玩具应用

本章我们要开发一个简单的演示应用,展示 Rails 强大的功能。我们会使用脚手架快速生成应用,这样就能站在一定高度上概览 Ruby on Rails 编程的过程(也能大致了解 Web 开发)。正如旁注 1.2 所说,本书将采用与众不同的方法,循序渐进开发一个完整的演示应用,遇到新的概念都会详细说明。不过为了快速概览(也为了寻找成就感),无需对脚手架避而不谈。我们将通过 URL 与最终开发出来的玩具应用交互,了解 Rails 应用的结构,也第一次演示 Rails 使用的 REST 架构。

与后面的演示应用类似,这个玩具应用中有用户(users)和微博(microposts),因此算是一个简化的 Twitter 类应用。应用的功能还需要后续开发,而且开发过程中的很多步骤看起来很神秘,不过暂时不用担心:从3.2 节起将从零开始再开发一个类似的完整应用,我还会提供大量的资料供你后续阅读。你要有些耐心,不要怕多犯错误,本章的主要目的就是让你不要被脚手架的神奇迷惑住,而要更深入地了解 Rails。

2.1 规划应用

这一节,我们要规划一下这个玩具应用。与 1.3 节一样,我们先使用 rails new 命令(指定 Rails 的版本号)生成应用的骨架:

$ cd ~/environment
$ rails _5.1.6_ new toy_app
$ cd toy_app/

如果使用1.2.1 节推荐的云端 IDE,这个应用可以在第一个应用所在的工作空间中创建,没必要再新建一个工作空间。如果没看到文件,可以点击文件浏览器中的齿轮图标,然后选择“Refresh File Tree”(刷新文件树)。

然后,在文本编辑器中修改 Gemfile 文件,写入代码清单 2.1 中的内容。

代码清单 2.1:这个玩具应用的 Gemfile 文件
source 'https://rubygems.org'

gem 'rails',        '5.1.6'
gem 'puma',         '3.9.1'
gem 'sass-rails',   '5.0.6'
gem 'uglifier',     '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks',   '5.0.1'
gem 'jbuilder',     '2.7.0'

group :development, :test do
  gem 'sqlite3', '1.3.13'
  gem 'byebug',  '9.0.6', platform: :mri
end

group :development do
  gem 'web-console',           '3.5.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.0.2'
  gem 'spring-watcher-listen', '2.0.1'
end

group :production do
  gem 'pg', '0.20.0'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

注意,代码清单 2.1代码清单 1.13 的内容一样。

1.5.1 节一样,安装 gem 时要指定 --without production 选项,不安装生产环境使用的 gem:

$ bundle install --without production

1.3.1 节所述,可能还要运行 bundle update旁注 1.1)。

最后,把这个玩具应用纳入 Git 版本控制系统:

$ git init
$ git add -A
$ git commit -m "Initialize repository"
create demo repo bitbucket
图 2.1:在 Bitbucket 中为这个玩具应用创建一个仓库

你还可以在 Bitbucket 中点击“Create”(新建)按钮创建一个新仓库图 2.1),然后把代码推送到这个远程仓库中:

$ git remote add origin git@bitbucket.org:<username>/toy_app.git
$ git push -u origin --all

越早部署应用越好。我建议你按照 1.3.4 节所述的步骤做,修改代码清单 2.2代码清单 2.3

代码清单 2.2:在 Application 控制器中添加 hello 动作
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
    render html: "hello, world!"
  end
end
代码清单 2.3:设置根路由
config/routes.rb
Rails.application.routes.draw do
  root 'application#hello'
end

然后,提交改动,再推送到 Heroku 中:

$ git commit -am "Add hello"
$ heroku create
$ git push heroku master

(与 1.5 节一样,你可能会看到一些提醒消息,现在先不去管它。7.5 节会解决。)除了 Heroku 为应用提供的地址之外,输出的内容应该与图 1.23 一样。

下面要开发这个应用了。一般来说,开发 Web 应用的第一步是创建数据模型(data model)。模型表示应用所需的结构。这个玩具应用是个Twitter 类微博,只有用户和简短的文章(微博)。那么,我们先为这个应用添加 User 模型(2.1.1 节),然后再添加 Micropost 模型(2.1.2 节)。

2.1.1 User 模型

网络中有多少不同的注册表单,就有多少定义用户数据模型的方式。简单起见,我们将使用一种最简可用的方式。这个玩具应用的用户有一个唯一的标识 idinteger 类型)、一个公开的名字 namestring 类型)和一个电子邮件地址 email(也是 string 类型)。电子邮件地址将作为用户名使用。User 模型的结构如图 2.2

demo user model
图 2.2User 数据模型

6.1.1 节会看到,图 2.2 中的 users 对应于数据库中的一个表(table);idnameemail 是表中的列(column)。

2.1.2 Micropost 模型

Micropost 数据模型比 User 模型还要简单:微博只要一个 id 和表示微博内容的 contenttext 类型)字段即可。[1]此外还有一个比较复杂的字段要实现,这个字段把微博和用户关联(associate)起来。我们使用 user_id 存储微博的属主。最终得到的 Micropost 数据模型如图 2.3 所示。

demo micropost model
图 2.3Micropost 数据模型

2.3.3 节会介绍怎样使用 user_id 字段简单实现一个用户拥有多个微博的功能。(第 13 章有更完整的说明。)

2.2 Users 资源

这一节我们要实现 2.1.1 节设定的 User 数据模型,还会为它创建 Web 界面。二者结合起来就是一个 Users 资源。“资源”的意思是把用户设想为对象,可以通过 HTTP 协议在网页中创建(create)、读取(read)、更新(update)和删除(delete)。正如前面提到的,我们将使用 Rails 内置的脚手架生成 Users 资源。我建议你先不要细看脚手架生成的代码,这时看只会让你更困惑。

scaffold 传给 rails generate 命令就可以使用 Rails 的脚手架了。传给 scaffold 的参数是资源名的单数形式(这里是 User[2],后面可以再跟着一些可选参数,指定数据模型中的字段:

$ rails generate scaffold User name:string email:string
      invoke  active_record
      create    db/migrate/20160515001017_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    test_unit
      create      test/controllers/users_controller_test.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/users.coffee
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.scss

我们在执行的命令中加入了 name:stringemail:string,这样就可以实现图 2.2 中的 User 模型了。注意,没必要指定 id 字段,Rails 会自动创建并将其设为表的主键(primary key)。

接下来我们要用 rails db:migrate 命令迁移(migrate)数据库,如代码清单 2.4 所示。

代码清单 2.4:迁移数据库
$ rails db:migrate
==  CreateUsers: migrating ====================================================
-- create_table(:users)
   -> 0.0017s
==  CreateUsers: migrated (0.0018s) ===========================================

上述命令的作用是使用新的 User 数据模型更新数据库。(从 6.1.1 节开始会深入学习数据库迁移。)

顺便说一下,在 Rails 5 之前的版本中,db:migrate 命令使用 rake 执行,而不是 rails。因此,如果你还要维护以前的应用,一定要知道如何使用 Rake(旁注 2.1)。

执行代码清单 2.4 中的迁移之后,可以新打开一个终端标签页(图 1.10),运行本地 Web 服务器:

$ rails server

现在,这个玩具应用应该可以在本地服务器中访问了,结果与 1.3.2 节一样。(如果使用云端 IDE,要在一个新的浏览器选项卡中打开网页,别在 IDE 中打开。)

2.2.1 浏览用户相关的页面

访问根 URL,我们会看到与图 1.15 一样的“hello, world!”页面。不过使用脚手架生成 Users 资源时生成了很多用来处理用户的页面。例如,列出所有用户的页面 /users,创建新用户的页面 /users/new。本节的目的是走马观花地浏览一下这些用户相关的页面。浏览时你会发现表 2.1 很有用,表中显示了页面和 URL 之间的对应关系。

表 2.1Users 资源中页面和 URL 的对应关系
URL 动作 作用

/users

index

列出所有用户

/users/1

show

显示 ID 为 1 的用户

/users/new

new

创建新用户

/users/1/edit

edit

编辑 ID 为 1 的用户

我们先来看显示应用中所有用户的页面,这个页面叫索引页,路径是 /users。和预期一样,目前还没有用户,如图 2.4 所示。

如果想创建新用户,要访问 /users/new 路径上的页面,如图 2.5 所示。第 7 章会把这个页面打造成用户注册页面。

我们可以在表单中填入名字和电子邮件地址,然后点击“Create User”(创建用户)按钮创建一个用户。 此时,浏览器会转向这个用户的页面,即 /users/1,如图 2.6 所示。(页面中显示的绿色文字是闪现消息(flash message),7.4.2 节会介绍。)注意,这个页面的 URL 是 /users/1。你可能猜到了,这里的 1 就是图 2.2 中的用户 id7.1 节会把这个页面打造成用户的资料页。

如果想修改用户的信息,要访问编辑页面,即 /users/1/edit(图 2.7)。修改用户信息后点击“Update User”(更新用户)按钮就更改了这个玩具应用中该用户的信息(图 2.8)。(第 6 章会详细介绍,用户的信息存储在后端的数据库中。)我们会在 10.1 节为演示应用添加编辑和更新用户信息的功能。

demo blank user index 3rd edition
图 2.4Users 资源的索引页(/users)
demo new user 3rd edition
图 2.5:新建用户页面(/users/new)
demo show user 3rd edition
图 2.6:显示某个用户的页面(/users/1)
demo edit user 3rd edition
图 2.7:编辑用户信息的页面(/users/1/edit)
demo update user 3rd edition
图 2.8:更新信息后的用户页面
demo user index two 3rd edition
图 2.9:创建第二个用户后的用户索引页(/users)

现在回到 /users/new 页面,在表单中填写信息,创建第二个用户。然后访问用户索引页,结果如图 2.9 所示。7.1 节会美化这个显示所有用户的页面。

我们已经看了创建、显示和编辑用户的页面,最后要看删除用户的页面(图 2.10)。点击图 2.10 中所示的链接后,会删除第二个用户,现在索引页面就只剩一个用户了。(如果这个操作不成功,确认浏览器是否启用了 JavaScript。Rails 通过 JavaScript 发送删除用户的请求。)10.4 节会为演示应用实现用户删除功能,而且仅限于管理员级别的用户才能执行这项操作。

demo destroy user 3rd edition
图 2.10:删除一个用户
练习
  1. (如果你了解 CSS)创建一个新用户,然后使用浏览器中的 HTML 审查工具找出“User was successfully created.”文本的 CSS ID。刷新页面后会发生什么?

  2. 如果创建用户时只填写名字,而没填写电子邮件地址,会发生什么?

  3. 如果创建用户时填写的电子邮件地址无效,例如填写的是“@example.com”,会发生什么?

  4. 删除前几题创建的用户。删除用户时,Rails 会显示消息吗?

2.2.2 MVC 实战

我们已经快速概览了 Users 资源,下面我们从 MVC(1.3.3 节)的视角出发,审视其中某些部分。我们将分析在浏览器中访问用户索引页(/users)的过程,了解一下 MVC(图 2.11)。

mvc detailed
图 2.11:Rails 中的 MVC 架构详解

图中各步的说明如下:

  1. 浏览器向 /users 发送请求;

  2. Rails 的路由把 /users 交给 Users 控制器的 index 动作处理;

  3. index 动作要求 User 模型检索所有用户(User.all);

  4. User 模型从数据库中读取所有用户;

  5. User 模型把所有用户组成的列表返回给控制器;

  6. 控制器把所有用户赋值给 @users 变量,然后传入 index 视图;

  7. 视图使用嵌入式 Ruby 把页面渲染成 HTML;

  8. 控制器把 HTML 送回浏览器。[3]

下面详细分析这个过程。首先,浏览器发送请求(第 1 步)。这一步可以直接在浏览器地址栏中输入地址,也可以点击网页中的链接。请求到达 Rails 路由器(第 2 步),路由器根据 URL(以及请求的类型,参见旁注 3.2)把请求分配给合适的控制器动作。把 Users 资源中相关的 URL 映射到控制器动作的代码如代码清单 2.5 所示。那行代码会按照表 2.1 中的对应关系做映射。(:users 这个写法看着很奇怪,它是一个符号(Symbol),4.3.3 节会介绍。)

代码清单 2.5:Rails 路由,为 Users 资源定义了一条规则
config/routes.rb
Rails.application.routes.draw do
  resources :users
  root 'application#hello'
end

既然打开路由文件了,那就花点儿时间把根路由改为用户索引页吧。修改之后,访问根地址就会显示 /users 页面。我们在代码清单 2.3 中添加了根路由:

root 'application#hello'

上述规则把根路由指向 Application 控制器中的 hello 动作。现在,我们想使用 Users 控制器的 index 动作,因此要按照代码清单 2.6 所示的代码修改。

代码清单 2.6:把根路由指向 Users 控制器的动作
config/routes.rb
Rails.application.routes.draw do
  resources :users
  root 'users#index'
end

一个控制器中有多个动作,2.2.1 节浏览的页面对应于 Users 控制器的不同动作。脚手架生成的控制器代码摘要如代码清单 2.7 所示。注意 class UsersController < ApplicationController 这种写法,在 Ruby 中这表示类继承。(2.3.4 节会简要介绍继承,4.4 节再做详细介绍。)

代码清单 2.7Users 控制器代码摘要
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end

你可能注意到了,动作的数量比我们看过的页面数量多,indexshownewedit 对应于 2.2.1 节介绍的页面。此外还有一些其他动作,createupdatedestroy。这些动作一般不直接渲染页面(不过有时也会),只会修改数据库中保存的用户数据。表 2.2 列出了控制器的全部动作,这些动作就是 Rails 对 REST 架构(旁注 2.2)的实现。REST 架构由计算机科学家 Roy Fielding 提出,意思是“表现层状态转化”(Representational State Transfer)。[4]注意表 2.2 中的内容,有些部分有重叠。例如 showupdate 两个动作都映射到 /users/1 这个地址上,二者的区别是响应的 HTTP 请求方法不同。3.3 节会更详细地介绍 HTTP 请求方法。

表 2.2代码清单 2.5Users 资源生成的符合 REST 架构的路由
HTTP 请求 URL 动作 作用

GET

/users

index

列出所有用户

GET

/users/1

show

显示 ID 为 1 的用户

GET

/users/new

new

显示创建新用户的页面

POST

/users

create

创建新用户

GET

/users/1/edit

edit

显示 ID 为 1 的用户的编辑页面

PATCH

/users/1

update

更新 ID 为 1 的用户

DELETE

/users/1

destroy

删除 ID 为 1 的用户

为了探明 Users 控制器与 User 模型之间的关系,我们看一下简化后的 index 动作,如代码清单 2.8 所示。(要阅读不完全能理解的代码也体现了“技术是复杂的”。)

代码清单 2.8:这个玩具应用中简化的 index 动作
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end

index 动作中有一行代码,@users = User.all图 2.11 中的第 3 步),让 User 模型从数据库中检索所有用户(第 4 步),然后把结果赋值给 @users 变量(读作“at-users”,第 5 步)。User 模型的代码参见代码清单 2.9。这段代码看似简单,但是通过继承具备了很多功能(参见 2.3.4 节4.4 节)。具体而言,使用 Rails 中名为 Active Record 的库后,User.all 就能返回数据库中的所有用户。

代码清单 2.9:玩具应用中的 User 模型
app/models/user.rb
class User < ActiveRecord::Base
end

定义 @users 变量后,控制器再调用视图(第 6 步)。视图的代码如代码清单 2.10 所示。以 @ 开头的变量是实例变量(instance variable),在视图中自动可用。这里,index.html.erb 视图中的代码(代码清单 2.10)遍历 @users,为每个用户生成一行 HTML。(你现在可能读不懂这些代码,这里只是让你看一下视图是什么样子。)

代码清单 2.10:用户索引页的视图
app/views/users/index.html.erb
<h1>Listing users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th colspan="3"></th>
    </tr>
  </thead>

<% @users.each do |user| %>
  <tr>
    <td><%= user.name %></td>
    <td><%= user.email %></td>
    <td><%= link_to 'Show', user %></td>
    <td><%= link_to 'Edit', edit_user_path(user) %></td>
    <td><%= link_to 'Destroy', user, method: :delete,
                                     data: { confirm: 'Are you sure?' } %></td>
  </tr>
<% end %>
</table>

<br>

<%= link_to 'New User', new_user_path %>

视图把代码转换成 HTML(第 7 步),然后控制器将其返回给浏览器,再显示出来(第 8 步)。

练习
  1. 参照图 2.11,写出访问 /users/1/edit 页面的步骤。

  2. 在脚手架生成的代码中找出前一题从数据库中检索用户的代码。

  3. 编辑用户页面的视图文件,其名称是什么?

2.2.3 这个 Users 资源的不足

脚手架生成的 Users 资源虽然能够让你大致了解 Rails,但也有一些不足:

  • 没有验证数据User 模型会接受空名字和无效的电子邮件地址,而不报错。

  • 没有验证身份。没实现登录和退出功能,随意一个用户都可以进行任何操作。

  • 没有测试。也不是完全没有,脚手架会生成一些基本的测试,不过很粗糙也不灵便,没有针对数据验证和身份验证的测试,更别说针对其他功能的测试了。

  • 没样式,没布局。没有共用的样式和网站导航。

  • 没真正理解。如果你能读懂脚手架生成的代码,就不需要阅读这本书了。

2.3 Microposts 资源

我们已经生成并浏览了 Users 资源,现在要生成 Microposts 资源。阅读本节时,我推荐你和 2.2 节对比一下。你会发现这两个资源在很多方面都是一致的。通过这样重复生成资源,我们可以更好地理解 Rails 中的 REST 架构。在这样的早期阶段看一下 Users 资源和 Microposts 资源的相同之处,也是本章的主要目的之一。

2.3.1 概览 Microposts 资源

Users 资源一样,我们将使用 rails generate scaffold 命令生成 Microposts 资源的代码,不过这一次要实现图 2.3 中的数据模型:[5]

$ rails generate scaffold Micropost content:text user_id:integer
      invoke  active_record
      create    db/migrate/20160515211229_create_microposts.rb
      create    app/models/micropost.rb
      invoke    test_unit
      create      test/models/micropost_test.rb
      create      test/fixtures/microposts.yml
      invoke  resource_route
       route    resources :microposts
      invoke  scaffold_controller
      create    app/controllers/microposts_controller.rb
      invoke    erb
      create      app/views/microposts
      create      app/views/microposts/index.html.erb
      create      app/views/microposts/edit.html.erb
      create      app/views/microposts/show.html.erb
      create      app/views/microposts/new.html.erb
      create      app/views/microposts/_form.html.erb
      invoke    test_unit
      create      test/controllers/microposts_controller_test.rb
      invoke    helper
      create      app/helpers/microposts_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/microposts/index.json.jbuilder
      create      app/views/microposts/show.json.jbuilder
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/microposts.coffee
      invoke    scss
      create      app/assets/stylesheets/microposts.scss
      invoke  scss
   identical    app/assets/stylesheets/scaffolds.scss

(如果看到 Spring 相关的错误,再次执行这个命令即可。)然后,跟 2.2 节一样,我们要执行迁移,更新数据库,使用新建的数据模型:

$ rails db:migrate
==  CreateMicroposts: migrating ===============================================
-- create_table(:microposts)
   -> 0.0023s
==  CreateMicroposts: migrated (0.0026s) ======================================

现在我们就可以使用类似 2.2.1 节中介绍的方法来创建微博了。你可能猜到了,脚手架还会更新 Rails 的路由文件,为 Microposts 资源加入一条规则,如代码清单 2.11 所示。[6]Users 资源类似,resources :micropsts 把微博相关的 URL 映射到 Microposts 控制器上,如表 2.3 所示。

代码清单 2.11:Rails 的路由,有一条针对 Microposts 资源的新规则
config/routes.rb
Rails.application.routes.draw do
  resources :microposts
  resources :users
  root 'users#index'
end
表 2.3代码清单 2.11Microposts 资源生成的符合 REST 架构的路由
HTTP 请求 URL 动作 作用

GET

/microposts

index

列出所有微博

GET

/microposts/1

show

显示 ID 为 1 的微博

GET

/microposts/new

new

显示创建新微博的页面

POST

/microposts

create

创建新微博

GET

/microposts/1/edit

edit

显示 ID 为 1 的微博的编辑页面

PATCH

/microposts/1

update

更新 ID 为 1 的微博

DELETE

/microposts/1

destroy

删除 ID 为 1 的微博

Microposts 控制器的代码简化后如代码清单 2.12 所示。注意,除了把 UsersController 换成 MicropostsController 之外,这段代码和代码清单 2.7 没什么区别。这说明了两个资源在 REST 架构中的共同之处。

代码清单 2.12:简化后的 Microposts 控制器
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
  def index
    .
    .
    .
  end

  def show
    .
    .
    .
  end

  def new
    .
    .
    .
  end

  def edit
    .
    .
    .
  end

  def create
    .
    .
    .
  end

  def update
    .
    .
    .
  end

  def destroy
    .
    .
    .
  end
end

我们在发布微博的页面(/microposts/new)输入一些内容,发布一篇微博,如图 2.12 所示。

既然已经打开这个页面了,那就多发布几篇微博,并且确保至少把一篇微博的 user_id 设为 1,把微博赋予 2.2.1 节中创建的第一个用户。结果应该和图 2.13 类似。

demo new micropost 3rd edition
图 2.12:发布微博的页面(/microposts/new)
demo micropost index 3rd edition
图 2.13:微博索引页(/microposts)
练习
  1. (如果你了解 CSS)发布一篇新微博,然后使用浏览器中的 HTML 审查工具找出“Micropost was successfully created.”文本的 CSS ID。刷新页面后会发生什么?

  2. 发布微博时不输入内容也不指定用户 ID 试试。

  3. 发布一篇内容超过 140 字(例如维基百科中介绍 Ruby 的第一段)的微博试试。

  4. 删除前几题创建的微博。

2.3.2 限制微博的长度

为了称得上“微博”这个名字,内容的长度要做限制。在 Rails 中实现这种限制很简单,使用验证(validation)功能即可。要限制微博的长度最多为 140 个字符(就像 Twitter 一样),我们可以使用长度验证。在文本编辑器或 IDE 中打开 app/models/micropost.rb 文件,写入代码清单 2.13 中的代码。

代码清单 2.13:限制微博的长度最多为 140 个字符
app/models/micropost.rb
class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140 }
end

这段代码看起来可能很神秘,我们会在 6.2 节详细介绍验证。如果我们在发布微博的页面输入超过 140 个字符的内容,就能看到这个验证的作用了。如图 2.14 所示,Rails 会渲染错误消息,提示微博的内容太长了。(7.3.3 节会详细介绍错误消息。)

micropost length error 3rd edition
图 2.14:发布微博失败时显示的错误消息
练习
  1. 使用前一节练习中的那段文字发布微博,这一次有什么变化呢?

  2. 使用浏览器中的 HTML 审查工具找到前一题那个错误消息的 CSS ID。

2.3.3 一个用户拥有多篇微博

Rails 最强大的功能之一,是可以在不同的数据模型之间建立关联(association)。对这里的 User 模型而言,每个用户可以拥有多篇微博。我们可以更新 User 模型(参见代码清单 2.14)和 Micropost 模型(参见代码清单 2.15)的代码实现这种关联。

代码清单 2.14:一个用户拥有多篇微博
app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
end
代码清单 2.15:一篇微博属于一个用户
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }
end

我们可以把这种关联用图 2.15 表示出来。因为 microposts 表中有 user_id 这一列,所以 Rails(通过 Active Record)能把微博和各个用户关联起来。

micropost user association
图 2.15:微博和用户之间的关联

第 13 章第 14 章,我们会使用微博和用户之间的关联显示用户的所有微博,还会生成一个和 Twitter 类似的微博列表。现在,我们可以在控制台(console)中检查用户与微博之间的关联。控制台是与 Rails 应用交互常用的工具。在命令行中执行 rails console 命令,启动控制台。然后输入 User.first,从数据库中检索第一个用户,并把得到的数据赋值给 first_user 变量:[7]

$ rails console
>> first_user = User.first
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> first_user.microposts
=> [#<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">, #<Micropost id: 2,
content: "Second micropost", user_id: 1, created_at: "2016-05-15 02:38:54",
updated_at: "2016-05-15 02:38:54">]
>> micropost = first_user.microposts.first    # 使用 Micropost.first 也可以
=> #<Micropost id: 1, content: "First micropost!", user_id: 1, created_at:
"2016-05-15 02:37:37", updated_at: "2016-05-15 02:37:37">
>> micropost.user
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.org",
created_at: "2016-05-15 02:01:31", updated_at: "2016-05-15 02:01:31">
>> exit

(我在这段代码的最后一行加上了 exit,告诉你如何退出控制台。在大多数系统中也可以按 Ctrl-D 键退出控制台。)[8]我们使用 first_user.microposts 获取这个用户发布的微博。Active Record 会自动返回 user_id 的值与 first_user 的 ID(1)相同的所有微博。在第 13 章第 14 章中,我们会更深入地学习关联。

练习
  1. 编辑显示用户的页面,显示用户发布的第一篇微博。(根据文件中的其他内容猜测所需的句法。)访问 /users/1,确认改动是正确的。

  2. 代码清单 2.16 添加了一个存在性验证,确保微博的内容不能为空。确认这个验证的行为与图 2.16 中一样。

  3. 代码清单 2.17 中的 FILL_IN 换成相应的代码,为 User 模型的 nameemail 属性添加存在性验证。效果如图 2.17 所示。

代码清单 2.16:验证微博内容存在性的代码
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 },
                      presence: true
end
代码清单 2.17:为 User 模型添加存在性验证
app/models/user.rb
class User < ApplicationRecord
  has_many :microposts
  validates FILL_IN, presence: true # 把 FILL_IN 换成正确的代码
  validates FILL_IN, presence: true # 把 FILL_IN 换成正确的代码
end
micropost content cant be blank
图 2.16:微博内容存在性验证的效果
user presence validations
图 2.17User 模型存在性验证的效果

2.3.4 继承体系

接下来简要介绍 Rails 中控制器和模型的类继承。 如果你有面向对象编程(Object-oriented Programming,简称 OOP)的经验,尤其是类,能更好地理解这些内容。如果暂时不理解,也没关系,4.4 节会详细说明这些概念。

我们先介绍模型的继承体系。对比一下代码清单 2.18代码清单 2.19,可以看出,UserMicropost 都(通过 < 符号)继承自 ApplicationRecord 类,而这个类继承自 ActiveRecord::Base 类,这是 Active Record 为模型提供的基类。图 2.18 列出了这种继承关系。继承 ActiveRecord::Base 类,模型对象才能与数据库通讯,才能把数据库中的列看做 Ruby 中的属性,等等。

代码清单 2.18User 类的继承关系
app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
end
代码清单 2.19Mcropost 类的继承关系
app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
end
demo model inheritance 4th ed
图 2.18User 模型和 Micropost 模型的继承体系

控制器的继承结构与模型基本相同。对比代码清单 2.20代码清单 2.21,可以看出,UsersControllerMicropostsController 都继承自 ApplicationController。如代码清单 2.22 所示,ApplicationController 继承自 ActionController::BaseActionController::Base 是 Rails 中 Action Pack 库为控制器提供的基类。这些类之间的关系如图 2.19 所示。

代码清单 2.20UsersController 类中的继承
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
end
代码清单 2.21MicropostsController 类中的继承
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
end
代码清单 2.22ApplicationController 类中的继承
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  .
  .
  .
end
demo controller inheritance
图 2.19UsersControllerMicropostsController 的继承体系

与模型的继承类似,通过继承 ActionController::BaseUsers 控制器和 Microposts 控制器获得了很多功能。例如,处理模型对象的能力、过滤入站 HTTP 请求,以及把视图渲染成 HTML 的能力。Rails 应用中的所有控制器都继承自 ApplicationController,所以其中定义的规则会自动运用于应用中的每个动作。例如,9.1 节会介绍如何在 Application 控制器中引入辅助方法,为整个应用的所有控制器都加上登录和退出功能。

练习
  1. 查看 Application 控制器文件的内容,找出 ApplicationController 继承自 ActionController::Base 的代码。

  2. ApplicationRecord 是不是也在类似的文件中继承 ActiveRecord::Base?提示:可能是 app/models 目录中名为 application_record.rb 的文件。

2.3.5 部署这个玩具应用

完成 Microposts 资源之后,是时候把代码推送到 Bitbucket 的仓库中了:

$ git status
$ git add -A
$ git commit -m "Finish toy app"
$ git push

通常情况下,你应该经常做一些很小的提交,不过对于本章来说,最后做一次大提交也无妨。

然后,你也可以参照 1.5 节所述的步骤,把这个应用部署到 Heroku 中:

$ git push heroku

(执行这个命令之前要按照 2.1 节中的说明创建 Heroku 应用:先执行 heroku create 命令,然后再执行 git push heroku master 命令。)

为了让应用能使用数据库,还要迁移生产数据库,方法是在代码清单 2.4 中那个迁移命令前面加上 heroku run

$ heroku run rails db:migrate

这个命令会按照 UserMicropost 数据模型更新 Heroku 中的数据库。迁移数据库之后,就可以在生产环境中使用这个应用了,如图 2.20 所示,而且这个应用使用 PostgreSQL 数据库。

toy app production
图 2.20:运行在生产环境中的玩具应用

最后,如果你做了 2.3.3 节练习,要把显示第一个用户微博的代码去掉,这样应用才能正确加载。你只需把那些代码删掉,做次提交,然后再推送到 Heroku。

练习
  1. 在线上应用中创建几个用户。

  2. 为第一个用户创建几篇微博。

  3. 发布微博时填写超过 140 个字,确认代码清单 2.13 中的验证在生产环境中可用。

2.4 小结

至此,对这个 Rails 应用的概览结束了。本章开发的玩具应用有优点也有缺点。

优点

  • 概览了 Rails

  • 介绍了 MVC

  • 第一次体验了 REST 架构

  • 开始使用数据模型了

  • 在生产环境中运行了一个基于数据库的 Web 应用

缺点

  • 没自定义布局和样式

  • 没有静态页面(例如首页和“关于”页面)

  • 没有用户密码

  • 没有用户头像

  • 没有登录功能

  • 不安全

  • 没实现用户和微博之间的自动关联

  • 没实现“关注”和“被关注”功能

  • 没实现微博列表

  • 没编写有意义的测试

  • 没有真正理解所做的事情

本书后续的内容建立在这些优点之上,而且会改善缺点。

2.4.1 本章所学

  • 使用脚手架自动生成模型的代码,然后通过 Web 界面与应用交互;

  • 脚手架利于快速上手,但生成的代码不易理解;

  • Rails 使用“模型-视图-控制器”(MVC)模式组织 Web 应用;

  • 借由 Rails 我们得知,为了与数据模型交互,REST 架构制定了一套标准的 URL 和控制器动作;

  • Rails 支持数据验证,用于约束数据模型的属性可以使用什么值;

  • Rails 内建支持建立数据模型关联的功能;

  • 可以使用 Rails 控制台在命令行中与 Rails 应用交互。

  1. 微博的内容很短,string 类型就足够了,但使用 text 类型更能表明我们的意图,而且也便于以后放宽微博长度限制。
  2. 脚手架中使用的名称与模型一样,是单数;而资源和控制器使用复数。因此,这里要使用 User,而不是 Users
  3. 有些文章说视图直接把 HTML 返回给浏览器(通过 Web 服务器,例如 Apache 或 Nginx)。不管实现的细节如何,我更相信控制器是一个中枢,应用中所有信息都会经由它传递。
  4. 加州大学欧文分校 2000 年 Roy Thomas Fielding 的博士论文《Architectural Styles and the Design of Network-based Software Architectures》。
  5. 与生成用户资源使用的脚手架命令一样,生成微博资源的脚手架也要使用单数形式,因此要用 generate Micropost
  6. 代码清单 2.11 相比,脚手架生成的代码可能会有额外的空行。无须担心,因为 Ruby 会忽略额外的空行。
  7. 你的控制台可能会显示类似 2.1.1 :001 > 的提示符,但示例中使用 >> 代替,因为不同的 Ruby 版本显示的提示符不同。
  8. 和“Ctrl-C”一样,这里大写的“D”指代键盘上的按键,不是大写字母“D”,因此按 Ctrl 键的同时不用按住 Shift 键。