11.2 显示微博

尽管我们还没实现直接在网页中发布微博的功能(将在 11.3.2 节实现),不过还是有办法显示微博,并对显示的内容进行测试。我们将按照 Twitter 的方式,不在微博资源的 index 页面显示用户的微博,而在用户资源的 show 页面显示,构思图如图 11.4 所示。我们会先使用一些简单的 ERb 代码,在用户的资料页面显示微博,然后在 9.3.2 节的种子数据中添加一些微博,这样才有内容可以显示。

user microposts mockup 3rd edition图 11.4:显示有微博的资料页面构思图

11.2.1 渲染微博

我们计划在用户的资料页面(show.html.erb)显示用户的微博,还要显示用户发布了多少篇微博。你会发现,很多做法和 9.3 节列出所有用户时类似。

虽然 11.3 节才会用到微博控制器,但马上就需要使用视图,所以现在就要生成控制器:

$ rails generate controller Microposts

这一节的主要目的是渲染用户发布的所有微博。9.3.5 节用过这样的代码:

<ul class="users">
  <%= render @users %>
</ul>

这段代码会自动使用局部视图 _user.html.erb 渲染 @users 变量中的每个用户。同样地,我们要编写 _micropost.html.erb 局部视图,使用类似的方式渲染微博集合:

<ol class="microposts">
  <%= render @microposts %>
</ol>

注意,我们使用的是有序列表标签 ol(而不是无需列表 ul),因为微博是按照一定顺序显示的(按时间倒序)。相应的局部视图如代码清单 11.21 所示。

代码清单 11.21:渲染单篇微博的局部视图

app/views/microposts/_micropost.html.erb

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
 <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
 <span class="content"><%= micropost.content %></span>
 <span class="timestamp">
 Posted <%= time_ago_in_words(micropost.created_at) %> ago.
 </span>
</li>

这个局部视图使用了 time_ago_in_words 辅助方法,这个方法的作用应该很明显,效果会在 11.2.2 节看到。代码清单 11.21 还为每篇微博指定了 CSS ID:

<li id="micropost-<%= micropost.id %>">

这是好习惯,说不定以后要处理(例如使用 JavaScript)单篇微博呢。

接下来要解决显示大量微博的问题。我们可以使用 9.3.3 节显示大量用户的方法来解决这个问题,即使用分页。和前面一样,我们要使用 will_paginate 方法:

<%= will_paginate @microposts %>

如果和用户列表页面的代码(代码清单 9.41)比较的话,会发现之前使用的代码是:

<%= will_paginate %>

前面之所以可以直接调用,是因为在用户控制器中,will_paginate 假定有一个名为 @users 的实例变量(9.3.3 节说过,这个变量所属的类应该是 AvtiveRecord::Relation)。现在,因为还在用户控制器中,但是我们要分页显示微博,所以必须明确地把 @microposts 变量传给 will_paginate 方法。当然了,我们还要在 show 动作中定义 @microposts 变量,如代码清单 11.22 所示。

代码清单 11.22:在用户控制器的 show 动作中定义 @microposts 变量

app/controllers/users_controller.rb

class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
 @microposts = @user.microposts.paginate(page: params[:page])  end
  .
  .
  .
end

注意看 paginate 方法是多么智能,甚至可以在关联上使用,从 microposts 表中取出每一页要显示的微博。

最后,还要显示用户发布的微博数量。我们可以使用 count 方法实现:

user.microposts.count

paginate 方法一样,count 方法也可以在关联上使用。count 的计数过程不是把所有微博都从数据库中读取出来,然后再在所得的数组上调用 length 方法,如果这样做的话,微博数量一旦很多,效率就会降低。其实,count 方法直接在数据库层计算,让数据库统计指定的 user_id 拥有多少微博。(所有数据库都会对这种操作做性能优化。如果统计数量仍然是应用的性能瓶颈,可以使用“计数缓存”进一步提速。)

综上所述,现在可以把微博添加到资料页面了,如代码清单 11.23 所示。注意,if @user.microposts.any?(在代码清单 7.19 中见过类似的用法)的作用是,如果用户没有发布微博,不显示一个空列表。

代码清单 11.23:在用户资料页面中加入微博

app/views/users/show.html.erb

<% provide(:title, @user.name) %>
<div class="row">
 <aside class="col-md-4">
 <section class="user_info">
 <h1>
  <%= gravatar_for @user %>
  <%= @user.name %>
 </h1>
 </section>
 </aside>
 <div class="col-md-8">
  <% if @user.microposts.any? %>
 <h3>Microposts (<%= @user.microposts.count %>)</h3>
 <ol class="microposts">
  <%= render @microposts %>
 </ol>
  <%= will_paginate @microposts %>
  <% end %>
</div> </div>

现在,我们可以查看一下修改后的用户资料页面,如图 11.5。可能会出乎你的意料,不过也是理所当然的,因为现在还没有微博。下面我们就来改变这种状况。

user profile no microposts 3rd edition图 11.5:添加显示微博的代码后用户的资料页面,但没有微博

11.2.2 示例微博

11.2.1 节,为了显示用户的微博,创建或修改了几个模板,但是结果有点不给力。为了改变这种状况,我们要在 9.3.2 节用到的种子数据中加入一些微博。

为所有用户添加示例微博要花很长时间,所以我们决定只为前六个用户添加。为此,要使用 take 方法:

User.order(:created_at).take(6)

调用 order 方法的作用是按照创建用户的顺序查找六个用户。

我们要分别为这六个用户创建 50 篇微博(数量要多于 30 个才能分页)。为了生成微博的内容,我们要使用 Faker 提供的 Lorem.sentence 方法。[2]添加示例微博后的种子数据如代码清单 11.24 所示。

代码清单 11.24:添加示例微博

db/seeds.rb

.
.
.
users = User.order(:created_at).take(6)
50.times do
  content = Faker::Lorem.sentence(5)
  users.each { |user| user.microposts.create!(content: content) }
end

然后,像之前一样重新把种子数据写入开发数据库:

$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed

完成后还要重启 Rails 开发服务器。

现在,我们能看到 11.2.1 节的劳动成果了——用户资料页面显示了微博。[3]初步结果如图 11.6 所示。

user profile microposts no styling 3rd edition图 11.6:用户资料页面显示的微博,还没添加样式

图 11.6 中显示的微博还没有样式,那我们就加入一些样式,如代码清单 11.25 所示,[4]然后再看一下页面显示的效果。

代码清单 11.25:微博的样式(包含本章要使用的所有 CSS)

app/assets/stylesheets/custom.css.scss

.
.
.
/* microposts */

.microposts {
  list-style: none;
  padding: 0;
  li {
    padding: 10px 0;
    border-top: 1px solid #e8e8e8;
  }
  .user {
    margin-top: 5em;
    padding-top: 0;
  }
  .content {
    display: block;
    margin-left: 60px;
    img {
      display: block;
      padding: 5px 0;
    }
  }
  .timestamp {
    color: $gray-light;
    display: block;
    margin-left: 60px;
  }
  .gravatar {
    float: left;
    margin-right: 10px;
    margin-top: 5px;
  }
}

aside {
  textarea {
    height: 100px;
    margin-bottom: 5px;
  }
}

span.picture {
  margin-top: 10px;
  input {
    border: 0;
  }
}

图 11.7 是第一个用户的资料页面,图 11.8 是另一个用户的资料页面,图 11.9 是第一个用户资料页面的第 2 页,页面底部还显示了分页链接。注意观察这三幅图,可以看到,微博后面显示了距离发布的时间(例如,“Posted 1 minute ago.”),这就是代码清单 11.21time_ago_in_words 方法实现的效果。过一会再刷新页面,这些文字会根据当前时间自动更新。

user profile with microposts 3rd edition图 11.7:显示有微博的用户资料页面(/users/1other profile with microposts 3rd edition图 11.8:另一个用户的资料页面(/users/5),也显示有微博user profile microposts page 2 3rd edition图 11.9:微博分页链接(/users/1?page=2

11.2.3 资料页面中微博的测试

新激活的用户会重定向到资料页面,那时已经测试了资料页面是否能正确渲染(代码清单 10.31)。本节,我们要编写几个简短的集成测试,检查资料页面中的其他内容。首先,生成资料页面的集成测试文件:

$ rails generate integration_test users_profile
      invoke  test_unit
      create    test/integration/users_profile_test.rb

为了测试资料页面中显示有微博,我们要把微博固件和用户关联起来。Rails 提供了一种便利的方法,可以在固件中建立关联,例如:

orange:
  content: "I  just  ate  an  orange!"
  created_at: <%= 10.minutes.ago %>
 user: michael

user 的值设为 michael 后,Rails 会把这篇微博和指定的用户固件关联起来:

michael:
  name: Michael Example
  email: [email protected]
  .
  .
  .

为了测试微博分页,我们要使用代码清单 9.43 中用到的方法,通过嵌入式 Ruby 代码多生成一些微博固件:

<% 30.times do |n| %>
micropost_<%= n %>:
  content: <%= Faker::Lorem.sentence(5) %>
  created_at: <%= 42.days.ago %>
  user: michael
<% end %>

综上,修改后的微博固件如代码清单 11.26 所示。

代码清单 11.26:添加关联用户后的微博固件

test/fixtures/microposts.yml

orange:
  content: "I  just  ate  an  orange!"
  created_at: <%= 10.minutes.ago %>
 user: michael 
tau_manifesto:
  content: "Check  out  the  @tauday  site  by  @mhartl:  http://tauday.com"
  created_at: <%= 3.years.ago %>
 user: michael 
cat_video:
  content: "Sad  cats  are  sad:  http://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>
 user: michael 
most_recent:
  content: "Writing  a  short  test"
  created_at: <%= Time.zone.now %>
 user: michael 
<% 30.times do |n| %> micropost_<%= n %>:
 content: <%= Faker::Lorem.sentence(5) %> created_at: <%= 42.days.ago %> user: michael <% end %>

测试数据准备好了,测试本身也很简单:访问资料页面,检查页面的标题、用户的名字、Gravatar 头像、微博数量和分页显示的微博,如代码清单 11.27 所示。注意,为了使用代码清单 4.2 中的 full_title 辅助方法测试页面的标题,我们要把 ApplicationHelper 模块引入测试。[5]

代码清单 11.27:用户资料页面的测试 GREEN

test/integration/users_profile_test.rb

require 'test_helper'

class UsersProfileTest < ActionDispatch::IntegrationTest
 include ApplicationHelper 
  def setup
    @user = users(:michael)
  end

  test "profile display" do
    get user_path(@user)
    assert_template 'users/show'
    assert_select 'title', full_title(@user.name)
    assert_select 'h1', text: @user.name
    assert_select 'h1>img.gravatar'
    assert_match @user.microposts.count.to_s, response.body
    assert_select 'div.pagination'
    @user.microposts.paginate(page: 1).each do |micropost|
      assert_match micropost.content, response.body
    end
  end
end

检查微博数量时用到了 response.body第 10 章的练习中见过。别被名字迷惑了,其实 response.body 的值是整个页面的 HTML 源码(不只是 body 元素中的内容)。如果我们只关心页面中某处显示的微博数量,使用下面的断言找到匹配的内容即可:

assert_match @user.microposts.count.to_s, response.body

assert_match 没有 assert_select 的针对性强,无需指定要查找哪个 HTML 标签。

代码清单 11.27 还在 assert_select 中使用了嵌套式句法:

assert_select 'h1>img.gravatar'

这行代码的意思是,在 h1 标签中查找类为 gravatarimg 标签。

因为应用能正常运行,所以测试组件应该也能通过:

代码清单 11.28:GREEN
$ bundle exec rake test