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:string
和 email: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 所示。
图 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 中的用户 id
。7.1 节会把这个页面打造成用户的资料页。
图 2.5:新建用户页面(/users/new)图 2.6:显示某个用户的页面(/users/1)
如果想修改用户的信息,要访问“编辑页面”(图 2.7)。修改用户信息后点击“Update User”(更新用户)按钮就更改了这个玩具应用中该用户的信息(图 2.8)。第 6 章会详细介绍,用户的信息存储在后端的数据库中。我们会在 9.1 节为演示应用添加编辑和更新用户信息的功能。
图 2.7:编辑用户信息的页面(/users/1/edit)
现在回到创建新用户的页面,提交表单创建第二个用户。然后访问用户索引页,结果如图 2.9 所示。7.1 节会美化这个显示所有用户的页面。
我们已经看了创建、显示和编辑用户的页面,最后要看删除用户的页面(图 2.10)。点击图 2.10 中所示的链接后,会删除第二个用户,索引页面就只剩一个用户了。如果这个操作不成功,确认浏览器是否启用了 JavaScript。Rails 通过 JavaScript 发起删除用户的请求。9.4 节会为演示应用实现用户删除功能,而且仅限于管理员级别的用户才能执行这项操作。
图 2.8:更新信息后的用户页面图 2.9:创建第二个用户后的用户索引页(/users)图 2.10:删除一个用户
2.2.2 MVC 实战
我们已经快速概览了用户资源,下面我们从 MVC(1.3.3 节)的视角出发,审视其中某些特定部分。我们要分析在浏览器中访问用户索引页的过程,了解一下 MVC(图 2.11)。
图中各步的说明如下:
浏览器向 /users 发起一个请求;
Rails 的路由把 /users 交给
UsersController
中的index
动作处理;index
动作要求用户模型读取所有用户(User.all
);用户模型从数据库中读取所有用户;
用户模型把所有用户组成的列表返回给控制器;
控制器把所有用户赋值给
@users
变量,然后传入index
视图;视图使用嵌入式 Ruby 把页面渲染成 HTML;
控制器把 HTML 发送回浏览器。[5]
图 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
你可能注意到了,动作的数量比我们看过的页面数量多,index
、show
、new
和 edit
对应于 2.2.1 节介绍的页面。不过还有一些其他动作,create
、update
和 destroy
等。这些动作一般不直接渲染页面(不过有时也会),只会修改数据库中保存的用户数据。表 2.2 列出了控制器的全部动作,这些动作就是 Rails 对 REST 架构(参见旁注 2.2)的实现。REST 架构由计算机科学家 Roy Fielding 提出,意思是“表现层状态转化”(Representational State Transfer)。[6]注意表 2.2 中的内容,有些部分有重叠。例如 show
和 update
两个动作都映射到 /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 请求方法(POST
,GET
,PATCH
[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,但也有一些不足:
没验证数据。用户模型会接受空名字和无效的电子邮件地址,而不报错。
没有认证机制。没实现登录和退出功能,随意一个用户都可以进行任何操作。
没有测试。也不是完全没有,脚手架会生成一些基本的测试,不过很粗糙也不灵便,没有针对数据验证和认证的测试,更别说针对其他功能的测试了。
没样式,没布局。没有共用的样式和网站导航。
没真正理解。如果你能读懂脚手架生成的代码,就不需要阅读这本书了。