# 12.2 關注用戶的網頁界面
[12.1 節](#the-relationship-model)用到了很多數據模型技術,可能要花些時間才能完全理解。其實,理解這些關聯最好的方式是在網頁界面中使用。
在本章的導言中,我們介紹了關注用戶的操作流程。本節,我們要實現這些構思的頁面,以及關注和取消關注功能。我們還會創建兩個頁面,分別列出我關注的用戶和關注我的用戶。在 [12.3 節](#the-status-feed),我們會實現用戶的動態流,屆時,這個演示應用才算完成。
## 12.2.1 示例數據
和之前的幾章一樣,我們要使用 Rake 任務把“關系”相關的種子數據加載到數據庫中。有了示例數據,我們就可以先實現網頁界面,本節末尾再實現后端功能。
“關系”相關的種子數據如[代碼清單 12.14](#listing-sample-relationships) 所示。我們讓第一個用戶關注第 3-51 個用戶,并讓第 4-41 個用戶關注第一個用戶。這樣的數據足夠用來開發應用的界面了。
##### 代碼清單 12.14:在種子數據中添加“關系”相關的數據
db/seeds.rb
```
# Users
User.create!(name: "Example User",
email: "example@railstutorial.org",
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](#listing-sample-relationships) 中的代碼:
```
$ bundle exec rake db:migrate:reset
$ bundle exec rake db:seed
```
## 12.2.2 數量統計和關注表單
現在示例用戶已經關注了其他用戶,也被其他用戶關注了,我們要更新一下用戶資料頁面和首頁,把這些變動顯示出來。首先,我們要創建一個局部視圖,在資料頁面和首頁顯示我關注的人和關注我的人的數量。然后再添加關注和取消關注表單,并且在專門的頁面中列出我關注的用戶和關注我的用戶。
[12.1.1 節](#a-problem-with-the-data-model-and-a-solution)說過,我們參照了 Twitter 的叫法,在我關注的用戶數量后使用“following”作標記(label),例如“50 following”。[圖 12.1](#fig-page-flow-profile-mockup) 中的構思圖就使用了這種表述方式,現在把這部分單獨摘出來,如[圖 12.10](#fig-stats-partial-mockup) 所示。
圖 12.10:數量統計局部視圖的構思圖
[圖 12.10](#fig-stats-partial-mockup) 中顯示的數量統計包含當前用戶關注的人數和關注當前用戶的人數,而且分別鏈接到專門的用戶列表頁面。在[第 5 章](chapter5.html#filling-in-the-layout),我們使用 `#` 占位符代替真實的網址,因為那時我們還沒怎么接觸路由。現在,雖然 [12.2.3 節](#following-and-followers-pages)才會創建所需的頁面,不過可以先設置路由,如[代碼清單 12.15](#listing-following-followers-actions-routes) 所示。這段代碼在 `resources` 塊中使用了 `:member` 方法。我們以前沒用過這個方法,你可以猜測一下這個方法的作用是什么。
##### 代碼清單 12.15:在用戶控制器中添加 `following` 和 `followers` 兩個動作
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](#listing-following-followers-actions-routes) 的作用確實如此。因為這兩個頁面都是用來顯示數據的,所以我們使用了 `get` 方法,指定這兩個地址響應的是 GET 請求。而且,使用 `member` 方法后,這兩個動作對應的 URL 地址中都會包含用戶的 ID。除此之外,我們還可以使用 `collection` 方法,但 URL 中就沒有用戶 ID 了。所以,如下的代碼
```
resources :users do
collection do
get :tigers
end
end
```
得到的 URL 是 /users/tigers(或許可以用來顯示應用中所有的老虎)。[[7](#fn-7)]
[代碼清單 12.15](#listing-following-followers-actions-routes) 生成的路由如[表 12.2](#table-following-routes) 所示。留意一下我關注的用戶頁面和關注我的用戶頁面的具名路由是什么,稍后會用到。
表 12.2:[代碼清單 12.15](#listing-following-followers-actions-routes) 中設置的規則生成的 REST 路由
| HTTP 請求 | URL | 動作 | 具名路由 |
| --- | --- | --- | --- |
| GET | /users/1/following | `following` | `following_user_path(1)` |
| GET | /users/1/followers | `followers` | `followers_user_path(1)` |
設好了路由后,我們來編寫數量統計局部視圖。我們要在一個 `div` 元素中顯示幾個鏈接,如[代碼清單 12.16](#listing-stats-partial) 所示。
##### 代碼清單 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](#listing-stats-partial) 的第一行,我們要獲取正確的用戶對象:
```
<% @user ||= current_user %>
```
我們在[旁注 8.1](chapter8.html#aside-or-equals)中介紹過這種用法,如果 `@user` 不是 `nil`(在用戶資料頁面),這行代碼沒什么效果;如果是 `nil`(在首頁),就會把當前用戶賦值給 `@user`。還有一處要注意,我關注的人數和關注我的人數是通過關聯獲取的,分別使用 `@user.following.count` 和 `@user.followers.count`。
我們可以和[代碼清單 11.23](chapter11.html#listing-user-show-microposts) 中獲取微博數量的代碼對比一下,微博的數量通過 `@user.microposts.count` 獲取。為了提高效率,Rails 會直接在數據庫層統計數量。
最后還有一個細節需要注意,某些元素指定了 CSS ID,例如:
```
<strong id="following" class="stat">
...
</strong>
```
這些 ID 是為 [12.2.5 節](#a-working-follow-button-with-ajax)中的 Ajax 準備的,因為 Ajax 要通過獨一無二的 ID 獲取頁面中的元素。
編寫好局部視圖,把它放入首頁就很簡單了,如[代碼清單 12.17](#listing-home-page-stats) 所示。
##### 代碼清單 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](#listing-stats-css) 所示(包含本章用到的所有樣式)。添加樣式后,首頁如[圖 12.11](#fig-home-page-follow-stats) 所示。
##### 代碼清單 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 */
.
.
.
```
圖 12.11:顯示有數量統計的首頁
稍后再把數量統計局部視圖添加到用戶資料頁面中,現在先來編寫關注和取消關注按鈕的局部視圖,如[代碼清單 12.19](#listing-follow-form-partial) 所示。
##### 代碼清單 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 %>
```
這段代碼其實也沒做什么,只是把具體的工作分配給 `follow` 和 `unfollow` 局部視圖了。我們要再次設置路由,加入“關系”資源,如[代碼清單 12.20](#listing-relationships-resource) 所示,和微博資源的設置類似([代碼清單 11.29](chapter11.html#listing-microposts-resource))。
##### 代碼清單 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
```
`follow` 和 `unfollow` 局部視圖的代碼分別如[代碼清單 12.21](#listing-follow-form) 和[代碼清單 12.22](#listing-unfollow-form) 所示。
##### 代碼清單 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](#listing-follow-form) 用來構建一個新“關系”,而[代碼清單 12.22](#listing-unfollow-form) 查找現有的“關系”。很顯然,第一個表單會向 `RelationshipsController` 發送 `POST` 請求,創建“關系”(`create` 動作);而第二個表單發送的是 `DELETE` 請求,銷毀“關系”(`destroy` 動作)。(這兩個動作在 [12.2.4 節](#a-working-follow-button-the-standard-way)編寫。)你可能還注意到了,關注用戶的表單中除了按鈕之外什么內容也沒有,但是仍然要把 `followed_id` 發送給控制器。在[代碼清單 12.21](#listing-follow-form) 中,我們使用 `hidden_field_tag` 方法把 `followed_id` 添加到表單中,生成的 HTML 如下:
```
<input id="followed_id" name="followed_id" type="hidden" value="3" />
```
[10.2.4 節](chapter10.html#resetting-the-password)說過,隱藏的 `input` 標簽會把所需的信息包含在表單中,但在瀏覽器中不會顯示出來。
現在我們可以在資料頁面中加入關注表單和數量統計了,如[代碼清單 12.23](#listing-user-follow-form-profile-stats) 所示,只需渲染相應的局部視圖即可。顯示有關注按鈕和取消關注按鈕的用戶資料頁面分別如[圖 12.12](#fig-profile-follow-button) 和[圖 12.13](#fig-profile-unfollow-button) 所示。
##### 代碼清單 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>
```
圖 12.12:某個用戶的資料頁面([/users/2](http://localhost:3000/users/2)),顯示有關注按鈕圖 12.13:某個用戶的資料頁面([/users/5](http://localhost:3000/users/5)),顯示有取消關注按鈕
稍后我們會讓這些按鈕起作用,而且要使用兩種方式實現,一種是常規方式([12.2.4 節](#a-working-follow-button-the-standard-way)),另一種使用 Ajax([12.2.5 節](#a-working-follow-button-with-ajax))。不過在此之前,我們要創建剩下的頁面——我關注的用戶列表頁面和關注我的用戶列表頁面。
## 12.2.3 我關注的用戶列表頁面和關注我的用戶列表頁面
我關注的用戶列表頁面和關注我的用戶列表頁面是資料頁面和用戶列表頁面混合體,在側邊欄顯示用戶的信息(包括數量統計),再列出一系列用戶。除此之外,還會在側邊欄中顯示一個用戶頭像列表。構思圖如[圖 12.14](#fig-following-mockup)(我關注的用戶)和[圖 12.15](#fig-followers-mockup)(關注我的用戶)所示。
圖 12.14:我關注的用戶列表頁面構思圖圖 12.15:關注我的用戶列表頁面構思圖
首先,我們要讓這兩個頁面的地址可訪問。按照 Twitter 的方式,訪問這兩個頁面都需要先登錄。我們要先編寫測試,參照以前的訪問限制測試,寫出的測試如[代碼清單 12.24](#listing-following-followers-authorization-test) 所示。
##### 代碼清單 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](#listing-following-followers-actions-routes) 中的路由設置,這兩個動作應該命名為 `following` 和 `followers`。在這兩個動作中,需要設置頁面的標題、查找用戶,獲取 `@user.followed_users` 或 `@user.followers`(要分頁顯示),然后再渲染頁面,如[代碼清單 12.25](#listing-following-followers-actions) 所示。
##### 代碼清單 12.25:`following` 和 `followers` 動作 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](#listing-following-followers-actions) 中的兩個動作都顯式調用了 `render` 方法,渲染一個名為 `show_follow` 的視圖。下面我們就來編寫這個視圖。這兩個動作之所以使用同一個視圖,是因為兩種情況用到的 ERb 代碼差不多,如[代碼清單 12.26](#listing-show-follow-view) 所示。
##### 代碼清單 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](#listing-following-followers-actions) 中的動作會按需渲染[代碼清單 12.26](#listing-show-follow-view) 中的視圖,分別顯式我關注的用戶列表和關注我的用戶列表,如[圖 12.16](#fig-user-following) 和[圖 12.17](#fig-user-followers) 所示。注意,上述代碼都沒有到“當前用戶”,所以這兩個鏈接對其他用戶也可用,如[圖 12.18](#fig-different-user-followers) 所示。
圖 12.16:顯示某個用戶關注的人圖 12.17:顯示關注某個用戶的人圖 12.18:顯示關注另一個用戶的人
現在,這兩個頁面可以使用了,下面要編寫一些簡短的集成測試,確認表現正確。這些測試只是健全檢查,無需面面俱到。正如 [5.3.4 節](chapter5.html#layout-link-tests)所說的,全面的測試,例如檢查 HTML 結構,并不牢靠,而且可能適得其反。對這兩個頁面來說,我們計劃確認顯示的數量正確,而且頁面中有指向正確的 URL 的鏈接。
首先,和之前一樣,生成一個集成測試文件:
```
$ rails generate integration_test following
invoke test_unit
create test/integration/following_test.rb
```
然后,準備測試數據。我們要在“關系”固件中創建一些關注關系。[11.2.3 節](chapter11.html#profile-micropost-tests)使用下面的代碼把微博和用戶關聯起來:
```
orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>
user: michael
```
注意,我們沒有用 `user_id: 1`,而是 `user: michael`。
按照這樣的方式編寫“關系”固件,如[代碼清單 12.27](#listing-relationships-fixtures) 所示。
##### 代碼清單 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](chapter11.html#listing-user-profile-test))。然后再檢查頁面中有沒有正確的鏈接,如[代碼清單 12.28](#listing-following-tests) 所示。
##### 代碼清單 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](#listing-relationships-controller) 中會看到,限制訪問這個控制器中的動作沒有太大的意義,但我們還是要加入安全機制。我們要在測試中確認,訪問這個控制器中的動作之前要先登錄(沒登錄就重定向到登錄頁面),而且數據庫中的“關系”數量沒有變化,如[代碼清單 12.30](#listing-relationships-access-control) 所示。
##### 代碼清單 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](#listing-relationships-controller) 所示。
##### 代碼清單 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](#listing-follow-form) 和[代碼清單 12.22](#listing-unfollow-form))對應的用戶,然后再調用[代碼清單 12.10](#listing-follow-unfollow-following) 中定義的 `follow` 或 `unfollow` 方法。各動作完整的實現如[代碼清單 12.32](#listing-relationships-controller-following) 所示。
##### 代碼清單 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 節](#following-tests)編寫集成測試檢查這些操作。)關注第二個用戶前后顯示的資料頁面如[圖 12.19](#fig-unfollowed-user) 和[圖 12.20](#fig-followed-user) 所示。
圖 12.19:關注前的資料頁面圖 12.20:關注后的資料頁面
## 12.2.5 關注按鈕的 Ajax 實現方式
雖然關注用戶的功能已經完全實現了,但在實現動態流之前,還有可以增強的地方。你可能已經注意到了,在 [12.2.4 節](#a-working-follow-button-the-standard-way)中,`RelationshipsController` 中的 `create` 和 `destroy` 動作最后都返回了一開始訪問的用戶資料頁面。也就是說,用戶 A 先訪問用戶 B 的資料頁面,點擊關注按鈕關注用戶 B,然后頁面立即又轉回到用戶 B 的資料頁面。因此,對這樣的流程我們有一個疑問:為什么要多一次頁面轉向呢?
Ajax [[8](#fn-8)]可以解決這種問題。Ajax 向服務器發送異步請求,在不刷新頁面的情況下更新頁面的內容。因為經常要在表單中處理 Ajax 請求,所以 Rails 提供了簡單的實現方式。其實,關注和取消關注表單局部視圖不用做大的改動,只要把 `form_for` 改成 `form_for…?, remote: true`,Rails 就會自動使用 Ajax 處理表單。這兩個局部視圖更新后的版本如[代碼清單 12.33](#listing-follow-form-ajax) 和[代碼清單 12.34](#listing-unfollow-form-ajax) 所示。
##### 代碼清單 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](http://railscasts.com/episodes/205-unobtrusive-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 請求,我們要在 `create` 和 `destroy` 動作([代碼清單 12.32](#listing-relationships-controller-following))中添加類似上面的 `respond_to` 塊,如[代碼清單 12.35](#listing-relationships-controller-ajax) 所示。注意,我們把本地變量 `user` 改成了實例變量 `@user`,因為在[代碼清單 12.32](#listing-relationships-controller-following) 中無需使用實例變量,而使用 Ajax 處理的表單([代碼清單 12.33](#listing-follow-form-ajax) 和[代碼清單 12.34](#listing-unfollow-form-ajax))則需要使用。
##### 代碼清單 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](#listing-relationships-controller-ajax) 中的代碼會優雅降級(不過要配置一個選項,如[代碼清單 12.36](#listing-degrade-gracefully) 所示),如果瀏覽器不支持 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.erb` 或 `destroy.js.erb`。你可能猜到了,在這種的文件中既可以使用 JavaScript 也可以使用嵌入式 Ruby 處理當前頁面。所以,為了更新關注后和取消關注后的頁面,我們要創建這種文件。
在 JS-ERb 文件中,Rails 自動提供了 [jQuery](http://jquery.com/) 庫的輔助函數,可以通過“[文檔對象模型](http://www.w3.org/DOM/)”(Document Object Model,簡稱 DOM)處理頁面中的內容。jQuery 庫中有很多處理 DOM 的方法,但現在我們只會用到其中兩個。首先,我們要知道通過 ID 獲取 DOM 元素的美元符號,例如,要獲取 `follow_form` 元素,可以使用如下的代碼:
```
$("#follow_form")
```
(參見[代碼清單 12.19](#listing-follow-form-partial),這個元素是包含表單的 `div`,而不是表單本身。)上面的句法和 CSS 一樣,`#` 符號表示 CSS 中的 ID。由此你可能猜到了,jQuery 和 CSS 一樣,使用點號 `.` 表示 CSS 中的類。
我們要使用的第二個方法是 `html`,使用指定的內容修改元素中的 HTML。例如,如果要把整個表單換成字符串 `"foobar"`,可以這么寫:
```
$("#follow_form").html("foobar")
```
和常規的 JavaScript 文件不同,JS-ERb 文件還可以使用嵌入式 Ruby 代碼。在 `create.js.erb` 文件中,(成功關注后)我們會把關注用戶表單換成取消關注用戶表單,并更新關注數量,如[代碼清單 12.37](#listing-create-js-erb) 所示。這段代碼中用到了 `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](#listing-destroy-js-erb) 所示。
##### 代碼清單 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](#listing-relationships-controller-ajax))。
取消關注的測試類似,只需把 `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](#listing-follow-button-tests) 所示。
##### 代碼清單 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
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習