2.2 用户资源

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

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

$ rails generate scaffold User name:string email:string
      invoke  active_record
      create    db/migrate/20140821011110_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
      create        test/helpers/users_helper_test.rb
      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.js.coffee
      invoke    scss
      create      app/assets/stylesheets/users.css.scss
      invoke  scss
      create    app/assets/stylesheets/scaffolds.css.scss

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

接下来我们要用 Rake(参见旁注 2.1)来迁移(migrate)数据库:

$ bundle exec rake db:migrate ==  CreateUsers: migrating ==============================================
-- create_table(:users)
   -> 0.0017s
==  CreateUsers: migrated (0.0018s) =====================================

上面的命令会使用新定义的用户数据模型更新数据库。(6.1.1 节会详细介绍数据库迁移)注意,为了使用 Gemfile 中指定的 Rake 版本,我们要通过 bundle exec 执行 rake。在很多系统中,包括云端 IDE,都不必使用 bundle exec,但某些系统必须使用,所以为了命令的完整,我会一直使用 bundle exec

然后,执行下面的命令,在另一个选项卡中运行本地 Web 服务器(图 1.7):[4]

$ rails server -b $IP -p $PORT  # 在本地设备中只需执行 `rails server`

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

旁注 2.1:Rake

在 Unix 中,把源码编译成可执行的程序时,make) 扮演了很重要的角色。很多程序员的身体甚至已经对下面的代码产生了条件反射:

$ ./configure && make && sudo make install

在 Unix 中(包括 Linux 和 Mac OS X),这个命令一般用来编译代码。

Rake 是 Ruby 版的 make,用 Ruby 语言编写的类 make 程序。Rails 灵活的运用了 Rake 的功能,提供了很多开发基于数据库的 Web 应用所需的管理任务。rake db:migrate 或许是最常用的。除此之外还有很多其他命令,运行 rake -T db 可以查看所有数据库相关的任务:

$ bundle exec rake -T db

如果想查看所有 Rake 任务,运行:

$ bundle exec rake -T

任务列表看起来有点让人摸不着头脑,不过现在无需担心,你不需要知道所有(甚至大多数)命令。学完本教程后,你会知道所有重要的任务。

2.2.1 浏览用户相关的页面

如果访问根 URL http://localhost:3000/ 看到的还是 Rails 默认页面(图 1.9)。不过使用脚手架生成用户资源时生成了很多用来处理用户的页面。例如,列出所有用户的页面地址是 /users,创建新用户的地址是 /users/new。本节的目的就是走马观花地浏览一下这些用户相关的页面。浏览时你会发现表 2.1 很有用,表中显示了页面和 URL 之间的对应关系。

表 2.1:用户资源中页面和 URL 的对应关系

URL 动作 作用
/users index 列出所有用户
/users/1 show 显示 ID 为 1 的用户
/users/new new 创建新用户
/users/1/edit edit 编辑 ID 为 1 的用户

我们先来看一下显示所有用户的页面,这个页面叫“索引页”。和预期一样,目前还没有用户,如图 2.4 所示。

demo blank user index 3rd edition图 2.4:用户资源的索引页(/users

如果想创建新用户要访问“新建用户”页面,如图 2.5 所示。(在本地开发时,地址的前面部分都是 http://localhost:3000 或云端 IDE 分配的地址,因此在后面的内容中我会省略这一部分。)第 7 章会把这个页面改造成用户注册页面。

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

demo new user 3rd edition图 2.5:新建用户页面(/users/newdemo show user 3rd edition图 2.6:显示某个用户的页面(/users/1

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

demo edit user 3rd edition图 2.7:编辑用户信息的页面(/users/1/edit

现在回到创建新用户的页面,提交表单创建第二个用户。然后访问用户索引页,结果如图 2.9 所示。7.1 节会美化这个显示所有用户的页面。

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

demo update user 3rd edition图 2.8:更新信息后的用户页面demo user index two 3rd edition图 2.9:创建第二个用户后的用户索引页(/usersdemo destroy user 3rd edition图 2.10:删除一个用户

2.2.2 MVC 实战

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

图中各步的说明如下:

  1. 浏览器向 /users 发起一个请求;

  2. Rails 的路由把 /users 交给 UsersController 中的 index 动作处理;

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

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

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

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

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

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

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

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

代码清单 2.2:Rails 路由,其中定义了用户资源的规则

config/routes.rb

Rails.application.routes.draw do
 resources :users  .
  .
  .
end

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

# root 'welcome#index'

改成了

root 'application#hello'

让根路由指向 ApplicationController 中的 hello 动作。现在我们想使用 UsersController 中的 index 动作,要按照代码清单 2.3 所示的方式修改。如果本章开头在 ApplicationController 中添加了 hello 动作,我建议现在把这个动作删除。

代码清单 2.3:把根路由指向 UsersController 中的动作

config/routes.rb

Rails.application.routes.draw do
  resources :users
 root 'users#index'  .
  .
  .
end

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

代码清单 2.4:用户控制器代码摘要

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)。[6]注意表 2.2 中的内容,有些部分有重叠。例如 showupdate 两个动作都映射到 /users/1 这个地址上。二者的区别是,使用的 HTTP 请求方法不同。3.3 节会更详细地介绍 HTTP 请求方法。

表 2.2:代码清单 2.2 生成的符合 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 的用户 |

旁注 2.2:表现层状态转化(REST)

如果你阅读过一些 Ruby on Rails Web 开发相关的资料,会看到很多地方都提到了“REST”,它是“表现层状态转化”(REpresentational State Transfer)的简称。REST 是一种架构方式,用来开发分布式、基于网络的系统和软件程序,例如 WWW 和 Web 应用。REST 理论很抽象,在 Rails 应用中,REST 意味着大多数组件(例如用户和微博)都会被模型化,变成资源(resource),可以创建(create)、读取(read)、更新(update)和删除(delete)。这些操作与关系型数据库中的 CRUD 操作HTTP 请求方法POSTGETPATCH [7] 和 DELETE)对应。3.3 节,特别是旁注 3.2,将更详细地介绍 HTTP 请求。

作为 Rails 应用开发者,REST 开发方式能帮助你决定编写哪些控制器和动作:你只需简单的把可以创建、读取、更新和删除的资源理清就可以了。对本章的“用户”和“微博”来说,这一过程非常明确,因为它们都是很自然的资源形式。在第 12 章将看到,使用 REST 架构可以通过一种自然而便捷的方式解决很棘手的问题(“关注用户”功能)。

为了探明用户控制器和用户模型之间的关系,我们看一下简化后的 index 动作,如代码清单 2.5 所示。(脚手架生成的代码很粗糙,所以我做了简化。)

代码清单 2.5:这个玩具应用中简化后的 index 动作

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def index
 @users = User.all  end
  .
  .
  .
end

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

代码清单 2.6:玩具应用中的用户模型

app/models/user.rb

class User < ActiveRecord::Base
end

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

代码清单 2.7:用户索引页的视图代码

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 步)。

2.2.3 这个用户资源的不足

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

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

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

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

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

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