ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 9.3 列出所有用户 本节,我们要添加倒数第二个用户控制器动作,`index`。`index` 动作不是显示某一个用户,而是显示所有用户。在这个过程中,我们要学习如何在数据库中生成示例用户数据,以及如何分页显示用户列表,让首页显示任意数量的用户。用户列表、分页链接和“Users”(所有用户)导航链接的构思图如[图 9.8](#fig-user-index-mockup) 所示。[[6](#fn-6)][9.4 节](#deleting-users)会添加管理功能,用来删除用户。 ![user index mockup bootstrap](https://box.kancloud.cn/2016-05-11_5733305f028fd.png)图 9.8:用户列表页面的构思图 ## 9.3.1 用户列表 创建用户列表之前,我们先要实现一个安全机制。单个用户的资料页面对网站的所有访问者开放,但要限制用户列表页面,只让已登录的用户查看,减少未注册用户能看到的信息量。[[7](#fn-7)] 为了限制访问 `index` 动作,我们先编写一个简短的测试,确认应用会正确重定向 `index` 动作,如[代码清单 9.31](#listing-index-action-redirected-test) 所示。 ##### 代码清单 9.31:测试 `index` 动作的重定向 RED test/controllers/users_controller_test.rb ``` require 'test_helper' class UsersControllerTest < ActionController::TestCase def setup @user = users(:michael) @other_user = users(:archer) end test "should redirect index when not logged in" do get :index assert_redirected_to login_url end . . . end ``` 然后我们要定义 `index` 动作,并把它加入被 `logged_in_user` 事前过滤器保护的动作列表中,如[代码清单 9.32](#listing-logged-in-user-index) 所示。 ##### 代码清单 9.32:访问 `index` 动作要先登录 GREEN app/controllers/users_controller.rb ``` class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] before_action :correct_user, only: [:edit, :update] def index end def show @user = User.find(params[:id]) end . . . end ``` 若要显示用户列表,我们要定义一个变量,存储网站中的所有用户,然后在 `index` 动作的视图中遍历,显示各个用户。你可能还记得玩具应用中相应的动作([2.5 节](chapter2.html#a-toy-app-exercises)),我们可以使用 `User.all` 从数据库中读取所有用户,然后把这些用户赋值给实例变量 `@users`,以便在视图中使用,如[代码清单 9.33](#listing-user-index) 所示。(你可能会觉得一次列出所有用户不太好,你是对的,我们会在 [9.3.3 节](#pagination)改进。) ##### 代码清单 9.33:用户控制器的 `index` 动作 app/controllers/users_controller.rb ``` class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.all end . . . end ``` 为了显示用户列表页面,我们要创建一个视图(要自己动手创建视图文件),遍历所有用户,把每个用户包含在一个 `li` 标签中。我们要使用 `each` 方法遍历所有用户,显示用户的 Gravatar 头像和名字,然后把所有用户包含在一个无序列表 `ul` 标签中,如[代码清单 9.34](#listing-user-index-view) 所示。 ##### 代码清单 9.34:`index` 视图 app/views/users/index.html.erb ``` <% provide(:title, 'All users') %> <h1>All users</h1> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> ``` 在[代码清单 9.34](#listing-user-index-view) 中,我们用到了 [7.7 节](chapter7.html#sign-up-exercises)练习中[代码清单 7.31](chapter7.html#listing-gravatar-option) 的成果,向 Gravatar 辅助方法传入第二个参数,指定头像的大小。如果你之前没有做这个练习,在继续阅读之前请参照[代码清单 7.31](chapter7.html#listing-gravatar-option),更新用户控制器的辅助方法文件。 然后再添加一些 CSS 样式(确切地说是 SCSS),如[代码清单 9.35](#listing-user-index-css)。 ##### 代码清单 9.35:用户列表页面的 CSS app/assets/stylesheets/custom.css.scss ``` . . . /* Users index */ .users { list-style: none; margin: 0; li { overflow: auto; padding: 10px 0; border-bottom: 1px solid $gray-lighter; } } ``` 最后,我们还要把头部导航中用户列表页面的链接地址换成 `users_path`,这是[表 7.1](chapter7.html#table-restful-users) 中还没用到的最后一个具名路由,如[代码清单 9.36](#listing-users-link) 所示。 ##### 代码清单 9.36:添加用户列表页面的链接地址 app/views/layouts/_header.html.erb ``` <header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", users_path %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", edit_user_path(current_user) %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: "delete" %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header> ``` 至此,用户列表页面完成了,所有的测试也都可以通过了: ##### 代码清单 9.37:**GREEN** ``` $ bundle exec rake test ``` 不过,如[图 9.9](#fig-user-index-only-one) 所示,页面中只显示了一个用户,有点孤单。下面,我们来改变这种悲惨状况。 ![user index only one 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f178f7.png)图 9.9:用户列表页面,只显示了一个用户 ## 9.3.2 示例用户 本节,我们要为应用添加更多的用户。为了让用户列表看上去像个“列表”,我们可以在浏览器中访问注册页面,一个一个地注册用户,不过还有更好的方法,让 Ruby(和 Rake)为我们创建用户。 首先,我们要在 `Gemfile` 中加入 `faker` gem,如[代码清单 9.38](#listing-faker-gemfile) 所示。这个 gem 会使用半真实的名字和电子邮件地址创建示例用户。(通常,可能只需在开发环境中安装 `faker` gem,但是对这个演示应用来说,生产环境也要使用 `faker`,参见 [9.5 节](#updating-showing-and-deleting-users-conclusion)。) ##### 代码清单 9.38:在 `Gemfile` 中加入 `faker` ``` source 'https://rubygems.org' gem 'rails', '4.2.2' gem 'bcrypt', '3.1.7' gem 'faker', '1.4.2' . . . ``` 然后和之前一样,运行下面的命令安装: ``` $ bundle install ``` 接下来,我们要添加一个 Rake 任务,向数据库中添加示例用户。Rails 使用一个标准文件 `db/seeds.rb` 完成这种操作,如[代码清单 9.39](#listing-db-seed) 所示。(这段代码涉及一些高级知识,现在不必太关注细节。) ##### 代码清单 9.39:向数据库中添加示例用户的 Rake 任务 db/seeds.rb ``` User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar") 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password) end ``` 在[代码清单 9.39](#listing-db-seed) 中,首先使用现有用户的名字和电子邮件地址创建一个示例用户,然后又创建了 99 个示例用户。其中,`create!` 方法和 `create` 方法的作用类似,只不过遇到无效数据时会抛出异常,而不是返回 `false`。这么做出现错误时不会静默,有利于调试。 然后,我们可以执行下述命令,还原数据库,再使用 `db:seed` 调用这个 Rake 任务:[[8](#fn-8)] ``` $ bundle exec rake db:migrate:reset $ bundle exec rake db:seed ``` 向数据库中添加数据的操作可能很慢,在某些系统中可能要花上几分钟。此外,有些读者反馈说,Rails 服务器运行的过程中无法执行 `reset` 命令,因此,可能要先停止服务器,然后再执行上述命令。 执行完 `db:seed` Rake 任务后,我们的应用中就有 100 个用户了,如[图 9.10](#fig-user-index-all) 所示。(可能要重启服务器才能看到效果。)我牺牲了一点个人时间,为前几个用户上传了头像,这样就不会都显示默认的 Gravatar 头像了。 ![user index all 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f32963.png)图 9.10:用户列表页面,显示了 100 个示例用户 ## 9.3.3 分页 现在,最初的那个用户不再孤单了,但是又出现了新问题:用户太多,全在一个页面中显示。现在的用户数量是 100 个,算是少的了,在真实的网站中,这个数量可能是以千计的。为了避免在一页中显示过多的用户,我们可以分页,一页只显示 30 个用户。 在 Rails 中有很多实现分页的方法,我们要使用其中一个最简单也最完善的,叫 [will_paginate](http://wiki.github.com/mislav/will_paginate/)。为此,我们要使用 `will_paginate` 和 `bootstrap-will_paginate` 这两个 gem。其中,`bootstrap-will_paginate` 的作用是设置 will_paginate 使用 Bootstrap 提供的分页样式。修改后的 `Gemfile` 如[代码清单 9.40](#listing-will-paginate-gem) 所示。 ##### 代码清单 9.40:在 `Gemfile` 中加入 `will_paginate` ``` source 'https://rubygems.org' gem 'rails', '4.2.2' gem 'bcrypt', '3.1.7' gem 'faker', '1.4.2' gem 'will_paginate', '3.0.7' gem 'bootstrap-will_paginate', '0.0.10' . . . ``` 然后执行下面的命令安装: ``` $ bundle install ``` 安装后还要重启 Web 服务器,确保成功加载这两个新 gem。 为了实现分页,我们要在 `index` 视图中加入一些代码,告诉 Rails 分页显示用户,而且要把 `index` 动作中的 `User.all` 换成知道如何分页的方法。我们先在视图中加入特殊的 `will_paginate` 方法,如[代码清单 9.41](#listing-will-paginate-index-view) 所示。稍后我们会看到为什么要在用户列表的前后都加入这个方法。 ##### 代码清单 9.41:在 `index` 视图中加入分页 app/views/users/index.html.erb ``` <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> <% end %> </ul> <%= will_paginate %> ``` `will_paginate` 方法有点小神奇,在用户控制器的视图中,它会自动寻找名为 `@users` 的对象,然后显示一个分页导航链接。[代码清单 9.41](#listing-will-paginate-index-view) 中的视图现在还不能正确显示分页,因为 `@users` 的值是通过 `User.all` 方法获取的([代码清单 9.33](#listing-user-index)),而 `will_paginate` 需要调用 `paginate` 方法才能分页: ``` $ rails console >> User.paginate(page: 1) User Load (1.5ms) SELECT "users".* FROM "users" LIMIT 30 OFFSET 0 (1.7ms) SELECT COUNT(*) FROM "users" => #<ActiveRecord::Relation [#<User id: 1,... ``` 注意,`paginate` 方法可以接受一个哈希参数,`:page` 键的值指定显示第几页。`User.paginate` 方法根据 `:page` 的值,一次取回一组用户(默认为 30 个)。所以,第一页显示的是第 1-30 个用户,第二页显示的是第 31-60 个,以此类推。如果 `:page` 的值为 `nil`,`paginate` 会显示第一页。 我们可以把 `index` 动作中的 `all` 方法换成 `paginate`,如[代码清单 9.42](#listing-will-paginate-index-action) 所示,这样就能分页显示用户了。`paginate` 方法所需的 `:page` 参数由 `params[:page]` 指定,`params` 中的这个键由 `will_pagenate` 自动生成。 ##### 代码清单 9.42:在 `index` 动作中分页取回用户 app/controllers/users_controller.rb ``` class UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update] . . . def index @users = User.paginate(page: params[:page]) end . . . end ``` 现在,用户列表页面应该可以显示分页了,如[图 9.11](#fig-user-index-pagination) 所示。(在某些系统中,可能需要重启 Rails 服务器。)因为我们在用户列表前后都加入了 `will_paginate` 方法,所以这两个地方都会显示分页链接。 ![user index pagination 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f4a488.png)图 9.11:分页显示的用户列表页面 如果点击链接“2”,或者“Next”,就会显示第二页,如[图 9.12](#fig-user-index-page-two-rails-3) 所示。 ![user index page two 3rd edition](https://box.kancloud.cn/2016-05-11_5733305f6c2f7.png)图 9.12:用户列表的第二页 ## 9.3.4 用户列表页面的测试 现在用户列表页面可以正常使用了,接下来要为这个页面编写一些简单的测试,其中一个测试前一节实现的分页。测试的步骤是,先登录,然后访问用户列表页面,确认第一页显示了一些用户,而且还显示了分页链接。为此,测试数据库中要有能足够数量的用户,足以分页才行,即超过 30 个。 我们在[代码清单 9.20](#listing-fixture-second-user) 中创建了第二个用户固件,但手动创建 30 多个用户,工作量有点大。不过,由固件中的 `password_digest` 属性得知,固件文件支持嵌入式 Ruby,所以我们可以使用[代码清单 9.43](#listing-users-fixtures-extra-users) 中的代码,再创建 30 个用户。([代码清单 9.43](#listing-users-fixtures-extra-users) 还多创建了几个用户,以备后用。) ##### 代码清单 9.43:在固件中再创建 30 个用户 test/fixtures/users.yml ``` michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> <% end %> ``` 然后,我们可以编写用户列表页面的测试了。首先,生成所需的测试文件: ``` $ rails generate integration_test users_index invoke test_unit create test/integration/users_index_test.rb ``` 在测试中,我们要检查是否有一个类为 `pagination` 的标签,以及第一页中是否显示了用户,如[代码清单 9.44](#listing-user-index-test) 所示。 ##### 代码清单 9.44:用户列表及分页的测试 GREEN test/integration/users_index_test.rb ``` require 'test_helper' class UsersIndexTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "index including pagination" do log_in_as(@user) get users_path assert_template 'users/index' assert_select 'div.pagination' User.paginate(page: 1).each do |user| assert_select 'a[href=?]', user_path(user), text: user.name end end end ``` 测试组件应该可以通过: ##### 代码清单 9.45:**GREEN** ``` $ bundle exec rake test ``` ## 9.3.5 使用局部视图重构 用户列表页面现在已经可以显示分页了,但是有个地方可以改进,我不得不介绍一下。Rails 提供了一些很巧妙的方法,可以精简视图的结构。本节我们要利用这些方法重构一下用户列表页面。因为我们已经做了很好的测试,所以可以放心重构,不必担心会破坏网站的功能。 重构的第一步,把[代码清单 9.41](#listing-will-paginate-index-view) 中的 `li` 换成 `render` 方法调用,如[代码清单 9.46](#listing-index-view-first-refactoring) 所示。 ##### 代码清单 9.46:重构用户列表视图的第一步 app/views/users/index.html.erb ``` <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <% @users.each do |user| %> <%= render user %> <% end %> </ul> <%= will_paginate %> ``` 在上述代码中,`render` 的参数不再是指定局部视图的字符串,而是代表 `User` 类的变量 `user`。[[9](#fn-9)]此时,Rails 会自定寻找一个名为 `_user.html.erb` 的局部视图。我们要手动创建这个视图,然后写入[代码清单 9.47](#listing-user-partial) 中的内容。 ##### 代码清单 9.47:显示单个用户的局部视图 app/views/users/_user.html.erb ``` <li> <%= gravatar_for user, size: 50 %> <%= link_to user.name, user %> </li> ``` 这个改进不错,不过我们还可以做得更好。我们可以直接把 `@users` 变量传给 `render` 方法,如[代码清单 9.48](#listing-index-final-refactoring) 所示。 ##### 代码清单 9.48:完全重构后的用户列表视图 GREEN app/views/users/index.html.erb ``` <% provide(:title, 'All users') %> <h1>All users</h1> <%= will_paginate %> <ul class="users"> <%= render @users %> </ul> <%= will_paginate %> ``` Rails 会把 `@users` 当作一个 `User` 对象列表,传给 `render` 方法后,Rails 会自动遍历这个列表,然后使用局部视图 `_user.html.erb` 渲染每个对象。重构后,我们得到了如[代码清单 9.48](#listing-index-final-refactoring) 这样简洁的代码。 每次重构修改应用代码后,都要运行测试组件确认仍能通过: ##### 代码清单 9.49:**GREEN** ``` $ bundle exec rake test ```