12.2 关注用户的网页界面

12.1 节用到了很多数据模型技术,可能要花些时间才能完全理解。其实,理解这些关联最好的方式是在网页界面中使用。

在本章的导言中,我们介绍了关注用户的操作流程。本节,我们要实现这些构思的页面,以及关注和取消关注功能。我们还会创建两个页面,分别列出我关注的用户和关注我的用户。在 12.3 节,我们会实现用户的动态流,届时,这个演示应用才算完成。

12.2.1 示例数据

和之前的几章一样,我们要使用 Rake 任务把“关系”相关的种子数据加载到数据库中。有了示例数据,我们就可以先实现网页界面,本节末尾再实现后端功能。

“关系”相关的种子数据如代码清单 12.14 所示。我们让第一个用户关注第 3-51 个用户,并让第 4-41 个用户关注第一个用户。这样的数据足够用来开发应用的界面了。

代码清单 12.14:在种子数据中添加“关系”相关的数据

db/seeds.rb

# Users
User.create!(name:  "Example  User",
             email: "[email protected]",
             password:              "foobar",
             password_confirmation: "foobar",
             admin:     true,
             activated: true,
             activated_at: Time.zone.now)

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,
              activated: true,
              activated_at: Time.zone.now)
end

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

# Following relationships users = User.all user  = users.first following = users[2..50] followers = users[3..40] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }

然后像之前一样,执行下面的命令,运行代码清单 12.14 中的代码:

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

12.2.2 数量统计和关注表单

现在示例用户已经关注了其他用户,也被其他用户关注了,我们要更新一下用户资料页面和首页,把这些变动显示出来。首先,我们要创建一个局部视图,在资料页面和首页显示我关注的人和关注我的人的数量。然后再添加关注和取消关注表单,并且在专门的页面中列出我关注的用户和关注我的用户。

12.1.1 节说过,我们参照了 Twitter 的叫法,在我关注的用户数量后使用“following”作标记(label),例如“50 following”。图 12.1 中的构思图就使用了这种表述方式,现在把这部分单独摘出来,如图 12.10 所示。

stats partial mockup图 12.10:数量统计局部视图的构思图

图 12.10 中显示的数量统计包含当前用户关注的人数和关注当前用户的人数,而且分别链接到专门的用户列表页面。在第 5 章,我们使用 # 占位符代替真实的网址,因为那时我们还没怎么接触路由。现在,虽然 12.2.3 节才会创建所需的页面,不过可以先设置路由,如代码清单 12.15 所示。这段代码在 resources 块中使用了 :member 方法。我们以前没用过这个方法,你可以猜测一下这个方法的作用是什么。

代码清单 12.15:在用户控制器中添加 followingfollowers 两个动作

config/routes.rb

Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
 resources :users do member do get :following, :followers end end  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
end

你可能猜到了,设定上述路由后,得到的 URL 地址类似 /users/1/following 和 /users/1/followers 这种形式。不错,代码清单 12.15 的作用确实如此。因为这两个页面都是用来显示数据的,所以我们使用了 get 方法,指定这两个地址响应的是 GET 请求。而且,使用 member 方法后,这两个动作对应的 URL 地址中都会包含用户的 ID。除此之外,我们还可以使用 collection 方法,但 URL 中就没有用户 ID 了。所以,如下的代码

resources :users do
  collection do
    get :tigers
  end
end

得到的 URL 是 /users/tigers(或许可以用来显示应用中所有的老虎)。[7]

代码清单 12.15 生成的路由如表 12.2 所示。留意一下我关注的用户页面和关注我的用户页面的具名路由是什么,稍后会用到。

表 12.2:代码清单 12.15 中设置的规则生成的 REST 路由

HTTP 请求 URL 动作 具名路由
GET /users/1/following following following_user_path(1)
GET /users/1/followers followers followers_user_path(1)

设好了路由后,我们来编写数量统计局部视图。我们要在一个 div 元素中显示几个链接,如代码清单 12.16 所示。

代码清单 12.16:显示数量统计的局部视图

app/views/shared/_stats.html.erb

<% @user ||= current_user %>
<div class="stats">
 <a href="<%= following_user_path(@user) %>">
 <strong id="following" class="stat">
  <%= @user.following.count %>
 </strong>
 following
 </a>
 <a href="<%= followers_user_path(@user) %>">
 <strong id="followers" class="stat">
  <%= @user.followers.count %>
 </strong>
 followers
 </a>
</div>

因为用户资料页面和首页都要使用这个局部视图,所以在代码清单 12.16 的第一行,我们要获取正确的用户对象:

<% @user ||= current_user %>

我们在旁注 8.1中介绍过这种用法,如果 @user 不是 nil(在用户资料页面),这行代码没什么效果;如果是 nil(在首页),就会把当前用户赋值给 @user。还有一处要注意,我关注的人数和关注我的人数是通过关联获取的,分别使用 @user.following.count@user.followers.count

我们可以和代码清单 11.23 中获取微博数量的代码对比一下,微博的数量通过 @user.microposts.count 获取。为了提高效率,Rails 会直接在数据库层统计数量。

最后还有一个细节需要注意,某些元素指定了 CSS ID,例如:

<strong id="following" class="stat">
...
</strong>

这些 ID 是为 12.2.5 节中的 Ajax 准备的,因为 Ajax 要通过独一无二的 ID 获取页面中的元素。

编写好局部视图,把它放入首页就很简单了,如代码清单 12.17 所示。

代码清单 12.17:在首页显示数量统计

app/views/static_pages/home.html.erb

<% if logged_in? %>
 <div class="row">
 <aside class="col-md-4">
 <section class="user_info">
  <%= render 'shared/user_info' %>
 </section>
 <section class="stats">
  <%= render 'shared/stats' %>
 </section>
 <section class="micropost_form">
  <%= render 'shared/micropost_form' %>
 </section>
 </aside>
 <div class="col-md-8">
 <h3>Micropost Feed</h3>
  <%= render 'shared/feed' %>
 </div>
 </div>
<% else %>
 .
 .
 .
<% end %>

我们要添加一些 SCSS 代码,美化数量统计,如代码清单 12.18 所示(包含本章用到的所有样式)。添加样式后,首页如图 12.11 所示。

代码清单 12.18:首页侧边栏的 SCSS 样式

app/assets/stylesheets/custom.css.scss

.
.
.
/* sidebar */
.
.
.
.gravatar {
  float: left;
  margin-right: 10px;
}

.gravatar_edit {
  margin-top: 15px;
}

.stats {
  overflow: auto;
  margin-top: 0;
  padding: 0;
  a {
    float: left;
    padding: 0 10px;
    border-left: 1px solid $gray-lighter;
    color: gray;
    &:first-child {
      padding-left: 0;
      border: 0;
    }
    &:hover {
      text-decoration: none;
      color: blue;
    }
  }
  strong {
    display: block;
  }
}

.user_avatars {
  overflow: auto;
  margin-top: 10px;
  .gravatar {
    margin: 1px 1px;
  }
  a {
    padding: 0;
  }
}

.users.follow {
  padding: 0;
}

/* forms */
.
.
.

home page follow stats 3rd edition图 12.11:显示有数量统计的首页

稍后再把数量统计局部视图添加到用户资料页面中,现在先来编写关注和取消关注按钮的局部视图,如代码清单 12.19 所示。

代码清单 12.19:显示关注或取消关注表单的局部视图

app/views/users/_follow_form.html.erb

<% unless current_user?(@user) %>
 <div id="follow_form">
  <% if current_user.following?(@user) %>
  <%= render 'unfollow' %>
  <% else %>
  <%= render 'follow' %>
  <% end %>
 </div>
<% end %>

这段代码其实也没做什么,只是把具体的工作分配给 followunfollow 局部视图了。我们要再次设置路由,加入“关系”资源,如代码清单 12.20 所示,和微博资源的设置类似(代码清单 11.29)。

代码清单 12.20:添加“关系”资源的路由设置

config/routes.rb

Rails.application.routes.draw do
  root                'static_pages#home'
  get    'help'    => 'static_pages#help'
  get    'about'   => 'static_pages#about'
  get    'contact' => 'static_pages#contact'
  get    'signup'  => 'users#new'
  get    'login'   => 'sessions#new'
  post   'login'   => 'sessions#create'
  delete 'logout'  => 'sessions#destroy'
  resources :users do
    member do
      get :following, :followers
    end
  end
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
 resources :relationships,       only: [:create, :destroy] end

followunfollow 局部视图的代码分别如代码清单 12.21代码清单 12.22 所示。

代码清单 12.21:关注用户的表单

app/views/users/_follow.html.erb

<%= form_for(current_user.active_relationships.build) do |f| %>
 <div><%= hidden_field_tag :followed_id, @user.id %></div>
  <%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
代码清单 12.22:取消关注用户的表单

app/views/users/_unfollow.html.erb

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete }) do |f| %>
  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

这两个表单都使用 form_for 处理“关系”模型对象,二者之间主要的不同点是,代码清单 12.21 用来构建一个新“关系”,而代码清单 12.22 查找现有的“关系”。很显然,第一个表单会向 RelationshipsController 发送 POST 请求,创建“关系”(create 动作);而第二个表单发送的是 DELETE 请求,销毁“关系”(destroy 动作)。(这两个动作在 12.2.4 节编写。)你可能还注意到了,关注用户的表单中除了按钮之外什么内容也没有,但是仍然要把 followed_id 发送给控制器。在代码清单 12.21 中,我们使用 hidden_field_tag 方法把 followed_id 添加到表单中,生成的 HTML 如下:

<input id="followed_id" name="followed_id" type="hidden" value="3" />

10.2.4 节说过,隐藏的 input 标签会把所需的信息包含在表单中,但在浏览器中不会显示出来。

现在我们可以在资料页面中加入关注表单和数量统计了,如代码清单 12.23 所示,只需渲染相应的局部视图即可。显示有关注按钮和取消关注按钮的用户资料页面分别如图 12.12图 12.13 所示。

代码清单 12.23:在用户资料页面加入关注表单和数量统计

app/views/users/show.html.erb

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

profile follow button 3rd edition图 12.12:某个用户的资料页面(/users/2),显示有关注按钮profile unfollow button 3rd edition图 12.13:某个用户的资料页面(/users/5),显示有取消关注按钮

稍后我们会让这些按钮起作用,而且要使用两种方式实现,一种是常规方式(12.2.4 节),另一种使用 Ajax(12.2.5 节)。不过在此之前,我们要创建剩下的页面——我关注的用户列表页面和关注我的用户列表页面。

12.2.3 我关注的用户列表页面和关注我的用户列表页面

我关注的用户列表页面和关注我的用户列表页面是资料页面和用户列表页面混合体,在侧边栏显示用户的信息(包括数量统计),再列出一系列用户。除此之外,还会在侧边栏中显示一个用户头像列表。构思图如图 12.14(我关注的用户)和图 12.15(关注我的用户)所示。

following mockup bootstrap图 12.14:我关注的用户列表页面构思图followers mockup bootstrap图 12.15:关注我的用户列表页面构思图

首先,我们要让这两个页面的地址可访问。按照 Twitter 的方式,访问这两个页面都需要先登录。我们要先编写测试,参照以前的访问限制测试,写出的测试如代码清单 12.24 所示。

代码清单 12.24:我关注的用户列表页面和关注我的用户列表页面的访问限制

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 following when not logged in" do
    get :following, id: @user
    assert_redirected_to login_url
  end

  test "should redirect followers when not logged in" do
    get :followers, id: @user
    assert_redirected_to login_url
  end
end

在实现这两个页面的过程中,唯一很难想到的是,我们要在用户控制器中添加相应的两个动作。按照代码清单 12.15 中的路由设置,这两个动作应该命名为 followingfollowers。在这两个动作中,需要设置页面的标题、查找用户,获取 @user.followed_users@user.followers(要分页显示),然后再渲染页面,如代码清单 12.25 所示。

代码清单 12.25:followingfollowers 动作 RED

app/controllers/users_controller.rb

class UsersController < ApplicationController
 before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers]  .
  .
  .
  def following
    @title = "Following"
    @user  = User.find(params[:id])
    @users = @user.following.paginate(page: params[:page])
    render 'show_follow'
  end

  def followers
    @title = "Followers"
    @user  = User.find(params[:id])
    @users = @user.followers.paginate(page: params[:page])
    render 'show_follow'
  end

  private
  .
  .
  .
end

读过本书前面的内容我们发现,按照 Rails 的约定,动作最后都会隐式渲染对应的视图,例如 show 动作最后会渲染 show.html.erb。而代码清单 12.25 中的两个动作都显式调用了 render 方法,渲染一个名为 show_follow 的视图。下面我们就来编写这个视图。这两个动作之所以使用同一个视图,是因为两种情况用到的 ERb 代码差不多,如代码清单 12.26 所示。

代码清单 12.26:渲染我关注的用户列表页面和关注我的用户列表页面的 show_follow 视图

app/views/users/show_follow.html.erb

<% provide(:title, @title) %>
<div class="row">
 <aside class="col-md-4">
 <section class="user_info">
  <%= gravatar_for @user %>
 <h1><%= @user.name %></h1>
 <span><%= link_to "view my profile", @user %></span>
 <span><b>Microposts:</b> <%= @user.microposts.count %></span>
 </section>
 <section class="stats">
  <%= render 'shared/stats' %>
  <% if @users.any? %>
 <div class="user_avatars">
  <% @users.each do |user| %>
  <%= link_to gravatar_for(user, size: 30), user %>
  <% end %>
 </div>
  <% end %>
 </section>
 </aside>
 <div class="col-md-8">
 <h3><%= @title %></h3>
  <% if @users.any? %>
 <ul class="users follow">
  <%= render @users %>
 </ul>
  <%= will_paginate %>
  <% end %>
 </div>
</div>

代码清单 12.25 中的动作会按需渲染代码清单 12.26 中的视图,分别显式我关注的用户列表和关注我的用户列表,如图 12.16图 12.17 所示。注意,上述代码都没有到“当前用户”,所以这两个链接对其他用户也可用,如图 12.18 所示。

user following 3rd edition图 12.16:显示某个用户关注的人user followers 3rd edition图 12.17:显示关注某个用户的人diferent user followers 3rd edition图 12.18:显示关注另一个用户的人

现在,这两个页面可以使用了,下面要编写一些简短的集成测试,确认表现正确。这些测试只是健全检查,无需面面俱到。正如 5.3.4 节所说的,全面的测试,例如检查 HTML 结构,并不牢靠,而且可能适得其反。对这两个页面来说,我们计划确认显示的数量正确,而且页面中有指向正确的 URL 的链接。

首先,和之前一样,生成一个集成测试文件:

$ rails generate integration_test following
      invoke  test_unit
      create    test/integration/following_test.rb

然后,准备测试数据。我们要在“关系”固件中创建一些关注关系。11.2.3 节使用下面的代码把微博和用户关联起来:

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

注意,我们没有用 user_id: 1,而是 user: michael

按照这样的方式编写“关系”固件,如代码清单 12.27 所示。

代码清单 12.27:“关系”固件

test/fixtures/relationships.yml

one:
  follower: michael
  followed: lana

two:
  follower: michael
  followed: malory

three:
  follower: lana
  followed: michael

four:
  follower: archer
  followed: michael

在这些固件中,Michael 关注了 Lana 和 Malory,Lana 和 Archer 关注了 Michael。为了测试数量,我们可以使用检查资料页面中微博数量的 assert_match 方法(代码清单 11.27)。然后再检查页面中有没有正确的链接,如代码清单 12.28 所示。

代码清单 12.28:测试我关注的用户列表页面和关注我的用户列表页面 GREEN

test/integration/following_test.rb

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
    log_in_as(@user)
  end

  test "following page" do
    get following_user_path(@user)
    assert_not @user.following.empty?
    assert_match @user.following.count.to_s, response.body
    @user.following.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end

  test "followers page" do
    get followers_user_path(@user)
    assert_not @user.followers.empty?
    assert_match @user.followers.count.to_s, response.body
    @user.followers.each do |user|
      assert_select "a[href=?]", user_path(user)
    end
  end
end

注意,在这段测试中有下面这个断言:

assert_not @user.following.empty?

如果不加入这个断言,下面这段代码就没有实际意义:

@user.following.each do |user|
  assert_select "a[href=?]", user_path(user)
end

(对关注我的用户列表页面的测试也是一样。)

测试组件应该可以通过:

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

12.2.4 关注按钮的常规实现方式

视图创建好了,下面我们要让关注和取消关注按钮起作用。因为关注和取消关注涉及到创建和销毁“关系”,所以我们需要一个控制器。像之前一样,我们使用下面的命令生成这个控制器:

$ rails generate controller Relationships

代码清单 12.31 中会看到,限制访问这个控制器中的动作没有太大的意义,但我们还是要加入安全机制。我们要在测试中确认,访问这个控制器中的动作之前要先登录(没登录就重定向到登录页面),而且数据库中的“关系”数量没有变化,如代码清单 12.30 所示。

代码清单 12.30:RelationshipsController 基本的访问限制测试 RED

test/controllers/relationships_controller_test.rb

require 'test_helper'

class RelationshipsControllerTest < ActionController::TestCase

  test "create should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      post :create
    end
    assert_redirected_to login_url
  end

  test "destroy should require logged-in user" do
    assert_no_difference 'Relationship.count' do
      delete :destroy, id: relationships(:one)
    end
    assert_redirected_to login_url
  end
end

RelationshipsController 中添加 logged_in_user 事前过滤器后,这个测试就能通过,如代码清单 12.31 所示。

代码清单 12.31:RelationshipsController 的访问限制 GREEN

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
  end

  def destroy
  end
end

为了让关注和取消关注按钮起作用,我们需要找到表单中 followed_id 字段(参见代码清单 12.21代码清单 12.22)对应的用户,然后再调用代码清单 12.10 中定义的 followunfollow 方法。各动作完整的实现如代码清单 12.32 所示。

代码清单 12.32:RelationshipsController 的代码

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
 user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user  end

  def destroy
 user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user  end
end

从这段代码中可以看出为什么前面说“限制访问没有太大意义”:如果未登录的用户直接访问某个动作(例如使用 curl 等命令行工具),current_user 的值是 nil,执行到这两个动作的第二行代码时会抛出异常,即得到一个错误,但对应用和数据来说都没危害。不过完全依赖这样的表现也不好,所以我们添加了一层安全防护措施。

现在,关注和取消关注功能都能正常使用了,任何用户都可以关注或取消关注其他用户。你可以在浏览器中点击相应的按钮验证一下。(我们会在 12.2.6 节编写集成测试检查这些操作。)关注第二个用户前后显示的资料页面如图 12.19图 12.20 所示。

unfollowed user图 12.19:关注前的资料页面followed user图 12.20:关注后的资料页面

12.2.5 关注按钮的 Ajax 实现方式

虽然关注用户的功能已经完全实现了,但在实现动态流之前,还有可以增强的地方。你可能已经注意到了,在 12.2.4 节中,RelationshipsController 中的 createdestroy 动作最后都返回了一开始访问的用户资料页面。也就是说,用户 A 先访问用户 B 的资料页面,点击关注按钮关注用户 B,然后页面立即又转回到用户 B 的资料页面。因此,对这样的流程我们有一个疑问:为什么要多一次页面转向呢?

Ajax [8]可以解决这种问题。Ajax 向服务器发送异步请求,在不刷新页面的情况下更新页面的内容。因为经常要在表单中处理 Ajax 请求,所以 Rails 提供了简单的实现方式。其实,关注和取消关注表单局部视图不用做大的改动,只要把 form_for 改成 form_for…​, remote: true,Rails 就会自动使用 Ajax 处理表单。这两个局部视图更新后的版本如代码清单 12.33代码清单 12.34 所示。

代码清单 12.33:使用 Ajax 处理关注用户的表单

app/views/users/_follow.html.erb

<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
 <div><%= hidden_field_tag :followed_id, @user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %> <% end %>
代码清单 12.34:使用 Ajax 处理取消关注用户的表单

app/views/users/_unfollow.html.erb

<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id),
             html: { method: :delete },
 remote: true) do |f| %>  <%= f.submit "Unfollow", class: "btn" %>
<% end %>

上述 ERb 代码生成的 HTML 没什么好说的,如果你好奇的话,可以看一下(细节可能不同):

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

可以看出,form 标签中设定了 data-remote="true",这个属性告诉 Rails,这个表单可以使用 JavaScript 处理。Rails 遵从了“非侵入式 JavaScript”原则(unobtrusive JavaScript),没有直接在视图中写入 JavaScript 代码(Rails 之前的版本直接写入了 JavaScript 代码),而是使用了一个简单的 HTML 属性。

修改表单后,我们要让 RelationshipsController 响应 Ajax 请求。为此,我们要使用 respond_to 方法,根据请求的类型生成合适的响应。例如:

respond_to do |format|
  format.html { redirect_to user }
  format.js
end

这种写法可能会让人困惑,其实只有一行代码会执行。(respond_to 块中的代码更像是 if-else 语句,而不是代码序列。)为了让 RelationshipsController 响应 Ajax 请求,我们要在 createdestroy 动作(代码清单 12.32)中添加类似上面的 respond_to 块,如代码清单 12.35 所示。注意,我们把本地变量 user 改成了实例变量 @user,因为在代码清单 12.32 中无需使用实例变量,而使用 Ajax 处理的表单(代码清单 12.33代码清单 12.34)则需要使用。

代码清单 12.35:在 RelationshipsController 中响应 Ajax 请求

app/controllers/relationships_controller.rb

class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    @user = User.find(params[:followed_id])
    current_user.follow(@user)
 respond_to do |format| format.html { redirect_to @user } format.js end  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow(@user)
 respond_to do |format| format.html { redirect_to @user } format.js end  end
end

代码清单 12.35 中的代码会优雅降级(不过要配置一个选项,如代码清单 12.36 所示),如果浏览器不支持 JavaScript,也能正常运行。

代码清单 12.36:添加优雅降级所需的配置

config/application.rb

require File.expand_path('../boot', __FILE__)
.
.
.
module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # 在处理 Ajax 的表单中添加真伪令牌
 config.action_view.embed_authenticity_token_in_remote_forms = true  end
end

当然,如果支持 JavaScript,也能正确的响应。如果是 Ajax 请求,Rails 会自动调用包含 JavaScript 的嵌入式 Ruby 文件(.js.erb),文件名和动作一样,例如 create.js.erbdestroy.js.erb。你可能猜到了,在这种的文件中既可以使用 JavaScript 也可以使用嵌入式 Ruby 处理当前页面。所以,为了更新关注后和取消关注后的页面,我们要创建这种文件。

在 JS-ERb 文件中,Rails 自动提供了 jQuery 库的辅助函数,可以通过“文档对象模型”(Document Object Model,简称 DOM)处理页面中的内容。jQuery 库中有很多处理 DOM 的方法,但现在我们只会用到其中两个。首先,我们要知道通过 ID 获取 DOM 元素的美元符号,例如,要获取 follow_form 元素,可以使用如下的代码:

$("#follow_form")

(参见代码清单 12.19,这个元素是包含表单的 div,而不是表单本身。)上面的句法和 CSS 一样,# 符号表示 CSS 中的 ID。由此你可能猜到了,jQuery 和 CSS 一样,使用点号 . 表示 CSS 中的类。

我们要使用的第二个方法是 html,使用指定的内容修改元素中的 HTML。例如,如果要把整个表单换成字符串 "foobar",可以这么写:

$("#follow_form").html("foobar")

和常规的 JavaScript 文件不同,JS-ERb 文件还可以使用嵌入式 Ruby 代码。在 create.js.erb 文件中,(成功关注后)我们会把关注用户表单换成取消关注用户表单,并更新关注数量,如代码清单 12.37 所示。这段代码中用到了 escape_javascript 方法,在 JavaScript 中写入 HTML 代码必须使用这个方法对 HTML 进行转义。

代码清单 12.37:创建“关系”的 JS-ERb 代码

app/views/relationships/create.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>")
$("#followers").html('<%= @user.followers.count %>')

destroy.js.erb 文件的内容类似,如代码清单 12.38 所示。

代码清单 12.38:销毁“关系”的 JS-ERb 代码

app/views/relationships/destroy.js.erb

$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>")
$("#followers").html('<%= @user.followers.count %>')

加入上述代码后,你应该访问用户资料页面,看一下关注或取消关注用户后页面是不是真的没有刷新。

12.2.6 关注功能的测试

关注按钮可以使用了,现在我们要编写一些简单的测试,避免回归。关注用户时,我们要向相应的地址发送 POST 请求,确认关注的人数增加了一个:

assert_difference '@user.following.count', 1 do
  post relationships_path, followed_id: @other.id
end

这是测试普通请求的方式,测试 Ajax 请求的方式基本类似,把 post 换成 xhr :post 即可:

assert_difference '@user.following.count', 1 do
  xhr :post, relationships_path, followed_id: @other.id
end

我们使用 xhr 方法(表示 XmlHttpRequest)发起 Ajax 请求,目的是执行 respond_to 块中对应于 JavaScript 的代码(代码清单 12.35)。

取消关注的测试类似,只需把 post 换成 delete。在下面的代码中,我们检查关注的人数减少了一个,而且指定了“关系”的 ID:

普通请求:

assert_difference '@user.following.count', -1 do
  delete relationship_path(relationship),
         relationship: relationship.id
end

Ajax 请求:

assert_difference '@user.following.count', -1 do
  xhr :delete, relationship_path(relationship),
               relationship: relationship.id
end

综上所述,测试如代码清单 12.39 所示。

代码清单 12.39:测试关注和取消关注按钮 GREEN

test/integration/following_test.rb

require 'test_helper'

class FollowingTest < ActionDispatch::IntegrationTest

  def setup
    @user  = users(:michael)
 @other = users(:archer)    log_in_as(@user)
  end
  .
  .
  .
  test "should follow a user the standard way" do
    assert_difference '@user.following.count', 1 do
      post relationships_path, followed_id: @other.id
    end
  end

  test "should follow a user with Ajax" do
    assert_difference '@user.following.count', 1 do
      xhr :post, relationships_path, followed_id: @other.id
    end
  end

  test "should unfollow a user the standard way" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      delete relationship_path(relationship)
    end
  end

  test "should unfollow a user with Ajax" do
    @user.follow(@other)
    relationship = @user.active_relationships.find_by(followed_id: @other.id)
    assert_difference '@user.following.count', -1 do
      xhr :delete, relationship_path(relationship)
    end
  end
end

测试组件应该能通过:

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