[Ruby]《Ruby on Rails Tutorial》的搬運工之三

ruby-on-rails-tutorials.jpg

背景:

  1. 最近比較閑,想學習ruby on rails
  2. 于是找到了https://www.railstutorial.org 上的首推教程《Ruby on Rails Tutorial》
    屏幕快照 2016-05-29 上午11.04.20.png

    這本書第一章和第二章講了2個基本demo,實在沒啥意思,姑且略過. 從第三章開始到第十二章是從0到1實現了一個類似Twitter的簡單社交網站(首頁,登錄注冊,發布推文,關注等功能). 怎么樣是不是很棒?
    但是這個本書實在講得過于詳細,對于我這種本身沒有那么多時間(也沒那么多耐心??)去一點一點看下來的童鞋,看著實在太著急了,于是準備快速整理下(把里面的干貨和代碼提取出來),方便大家可以分分鐘coding出這個demo出來.
    當然真正學習還是要看原教程,我這個只是"扒皮版本".

<br />

原文鏈接

RUBY ON RAILS TUTORIAL
https://www.railstutorial.org/book/static_pages

他們的github:

railstutorial/sample_app_rails_4
https://github.com/railstutorial/sample_app_rails_4

<br />

ruby學習框架圖

ruby on rails is hard?

第3-7章節見:

[Ruby]RUBY ON RAILS TUTORIAL 的搬運工之一

第8-10章節見:

[Ruby]《Ruby on Rails Tutorial》的搬運工之二

<br />

下面是第11章開始


11. User microposts

用戶的推文功能實現:

11.1 A Micropost model

a). 首先我們需要新建一個基本model

rails generate model Micropost content:text user:references
屏幕快照 2016-05-31 下午3.30.16.png

b). 數據增加index

//db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, index: true, foreign_key: true

      t.timestamps null: false
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

11.1.3 User/Micropost associations

做數據關聯:


User/Micropost associations

polen:
這個是ruby的一個特性.ruby希望我們的數據庫原則上不要做強關聯(當然你非要這么干也沒人攔著你),但是它提供了一種模型關聯的方法,這種關聯方法有什么用? 就是數據之間的操作可以更簡單也更直觀一些:
舉例:我們有2個model,customer和order.
如果不做關聯,新建個訂單需要:

@order = Order.create(order_date: Time.now, customer_id: @customer.id)

但二者如果做了belongs_to和has_many關聯,那么就可以寫成:

@order = @customer.orders.create(order_date: Time.now)

比較常見的幾種關聯關系有:

belongs_to  # 一對多,與 has_many,has_one 套用        
has_one      # 一對一          
has_many   # 一對多的另外一方            
has_and_belongs_to_many # 多對多

參照:Rubyonrails.org:Active Record 關聯

11.1.4 Micropost refinements

這里是對11.1.3的完善
a). 用戶的推文信息很多的時候,我們希望默認是按照"最近發布的排在最前面"的原則(即:時間倒序)
怎么實現呢? 很簡單,設置個default_scope.

class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

b). 用戶和自己的推文是綁定的,如果用戶被刪除了,那么他對應的推文也需要全部刪除,這個總不能來個for循環刪除吧,那怎么辦呢?
剛在不是做了關聯么,加一句話即可dependent: :destroy

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  attr_accessor :remember_token, :activation_token, :reset_token
...

polen:
dependent用于:設置銷毀擁有者時要怎么處理關聯對象
包含以下幾種參數:

  • destroy:也銷毀關聯對象;
  • delete:直接把關聯對象對數據庫中刪除,因此不會執行回調;
  • nullify:把外鍵設為 NULL,不會執行回調;
  • restrict_with_exception:有關聯的對象時拋出異常;
  • restrict_with_error:有關聯的對象時,向擁有者添加一個錯誤;

如果在數據庫層設置了 NOT NULL約束,就不能使用 :nullify
選項。如果 :dependent選項沒有銷毀關聯,就無法修改關聯對象,因為關聯對象的外鍵設置為不接受 NULL.

11.2 Showing microposts

數據層做好了,開始做UI層展示了:

產品經理給的草圖

11.2.1 Rendering micro posts

a). 新建controller,新建html

//新建一個controller
rails generate controller Microposts

//新建_micropost.html.erb,參照之前的_user.html.erb代碼模式
touch app/views/microposts/_micropost.html.erb

b). 畫基礎UI (相當于iOS的tableviewCell )

//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>

c). 界面展示controller和show.html.erb:

//app/controllers/users_controller.rb
...
  def show
    @user = User.find(params[:id])
    @micropost = @user.miscropost.paginate(page: params[:page])
  end
...
//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.micropost.any? %>
      <h3>Miscroposts (<%= @user.micropost.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>      
    <% end %>
  </div>
</div>

d). 布局調整一下,不然目前是這樣的


屏幕快照 2016-05-31 下午5.05.32.png
//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;
  }
}

調整之后是這樣子:


屏幕快照 2016-05-31 下午5.07.47.png

11.3 Manipulating microposts

接下來就是對推文的增刪操作了.
做之前先增加路由:

//config/routes.rb
...
 resources :microposts, only: [:create, :destroy]

11.3.1 Micropost access control

a). 首先要查看推文的話,需要檢查是否登錄,之前是因為只是用戶信息界面需要,所以我們的logged_in_user方法只寫在了users_controller里面,現在作為公用,我們需要移到application_controller里面

//app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include  SessionsHelper

  private

  # Confirms a logged-in user.
  def logged_in_user
    unless logged_in?
      store_location
      flash[:danger] = "Please log in."
      redirect_to login_url
    end
  end
end

b). 作為公用之后,我們就可以用起來了:
MicropostsController增加action,以及對應的登錄狀態檢查

//app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create 
  end 

  def destroy
  end
end

11.3.2 Creating microposts

a). controller 中添加create action

//app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      render 'static_pages/home'
    end
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
end

b). home 頁添加發布消息的UI

//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="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
  </div>
<% else %>
  <div class="center jumbotron">
    <h1>Welcome to the Sample App</h1>

    <h2>
      This is the home page for the
      <a >Ruby on Rails Tutorial</a>
      sample application.
    </h2>

    <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
  </div>

  <%= link_to image_tag("rails.png", alt: "Rails logo"),
              'http://rubyonrails.org/' %>
<% end %>

c). 新增_user_info.html.erb和_micropost_form.html.erb

//app/views/shared/_user_info.html.erb
<%= link_to gravatar_for(current_user, size: 50), current_user %>
<h1><%= current_user.name %></h1>
<span><%= link_to "view my profile", current_user %></span>
<span><%= pluralize(current_user.microposts.count, "micropost") %></span>
//app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
<% end %>

d). StaticPagesController添加一個變量micropost

//app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
  def home
    @micropost = current_user.microposts.build if logged_in?
  end

e) .錯誤提示之前只是用于user,現在需要修改為通用型,所以將@user修改為object:

//app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>.
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

然后將

  • users/new.html.erb,
  • users/edit.html.erb,
  • password_resets/edit.html.erb

中的錯誤處理:

 <%= render 'shared/error_messages' %>

修改為:

 <%= render 'shared/error_messages', object: f.object %>

11.3.3 A porto-feed

polen:
feed這個東西是什么鬼?也想不出具體的定義,其實就是個list .想深入了解的童鞋可以參考:
知乎:Feed 除了 timeline 形式,還有沒有更好的內容展示方式...

a). user model 增加def feed

//app/models/user.rb
class User < ActiveRecord::Base
...
  # Defines a proto-feed.
  # See "Following users" for the full implementation.
  def feed
    Micropost.where("user_id = ?", id)
  end

  private
...

b). static_pages_controller 增加@feed_items

//app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
  def home
    if logged_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end

  end
...

c). 寫個可復用的_feed.html.erb

//app/views/shared/_feed.html.erb
<% if @feed_items.any? %>
  <ol class="microposts">
    <%= render @feed_items %>
  </ol>
  <%= will_paginate @feed_items %>
<% end %>

d). home頁把feed加進去

//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="micropost_form">
        <%= render 'shared/micropost_form' %>
      </section>
    </aside>
    <div class="col-md-8">
      <h3>Micropost Feed</h3>
      <%= render 'shared/feed' %>
    </div>    
  </div>
<% else %>
...

e). 針對提交失敗,@feed_items找不到,所以要打個預防針:

//app/controllers/microposts_controller.rb
  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

11.3.4 Destroying microposts

這塊開始搞刪除啦...

a). 增加個刪除的按鈕

//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.
    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>
  </span>
</li>

b). controller 中增加def destroydef correct_user

//app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy

  def create
    @micropost = current_user.microposts.build(micropost_params)
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_url
    else
      @feed_items = []
      render 'static_pages/home'
    end
  end

  def destroy
    @micropost.destroy
    flash[:success] = "Micropost deleted"
    redirect_to request.referrer || root_url
  end

  def destroy
  end

  private

    def micropost_params
      params.require(:micropost).permit(:content)
    end
    
    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end
end

11.4 Micropost images

沒有圖像,用戶還是怎么"裝高冷"?...

11.4.1 Basic image upload

a). 首先引入幾個庫

gem 'carrierwave',             '0.10.0'
gem 'mini_magick',             '3.8.0'
gem 'fog',                     '1.36.0'

b). 建一個uploader以及數據庫加字段picture:string

rails generate uploader Picture

rails generate migration add_picture_to_microposts picture:string

c). model 中加image

//app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

polen:
這里用到了mount_uploader這個方法,這個方法用于將model的屬性和上傳者綁定.(其實就是綁定數據庫的某一列)
官方解釋看這里:/CarrierWave/Mount

d). UI可以改起來啦,在"寫新推文"的界面,添加上傳照片的button

//app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture %>
  </span>
<% end %>

e). MicropostsController中參數也要把picture帶進去

//app/controllers/microposts_controller.rb
   def micropost_params
      params.require(:micropost).permit(:content, :picture)
    end

f). 繼續改UI,找個展示圖片的地方

//app/views/microposts/_micropost.html.erb
...
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %>
  </span>
...

11.4.2 Image validation

以防用戶亂傳各種蒼老師的照片,我們需要加一些限制和校驗??...

a). 格式限制:picture_uploader.rb中關于格式限制的代碼解注

//app/uploaders/picture_uploader.rb
 def extension_white_list
    %w(jpg jpeg gif png)
  end

b). 大小限制

//app/models/micropost.rb
class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order(created_at: :desc) }
  mount_uploader :picture, PictureUploader
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
  validate  :picture_size

  private

    # Validates the size of an uploaded picture.
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end
end

如果超過大小限制,我們需要來個alert提示.

//app/views/shared/_micropost_form.html.erb
<%= form_for(@micropost, html: { multipart: true }) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/gif,image/png' %>
  </span>
<% end %>

<script type="text/javascript">
  $('#micropost_picture').bind('change', function() {
    var size_in_megabytes = this.files[0].size/1024/1024;
    if (size_in_megabytes > 5) {
      alert('Maximum file size is 5MB. Please choose a smaller file.');
    }
  });
</script>

11.4.3 Image resizing

調整大小,不然丑的不得了...(簡書圖片默認都是width=1240)

PictureUploader中加一下限制即可:
首先要安裝一下imagemagick:

brew install imagemagick
brew install imagemagick

然后resize_to_limit進行限制:

//app/uploaders/picture_uploader.rb
class PictureUploader < CarrierWave::Uploader::Base

  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick
  process resize_to_limit: [400, 400]
...

可以看一下,限制前后的對比:


屏幕快照 2016-05-31 下午8.08.54.png

12. Following users

真正的社交屬性來了啊

12.1 The Relationship model

關注(或者說互粉)這種功能,按照之前的邏輯是建個表,userA,has_many(model的關聯屬性) user.following表。但是這種的缺點是二者的耦合性太強,一旦用戶改個名字或者什么,那對應的表都要修改,這種影響的范圍太大. 于是考慮到,中和下,建立個關聯表,作為中間過渡,這樣可以確保,用戶之間的關聯只通過這個relationship表來查找,而不影響user.following表. 多說無意,一圖解真相:

屏幕快照 2016-05-31 下午8.19.23.png

具體做起來:
a). 生成一個Relationship的model:

rails generate model Relationship follower_id:integer followed_id:integer

b). 修改表結構,增加幾個字段:

//db/migrate/[timestamp]_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps null: false
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end

c). 數據庫遷移

rake db:migrate

12.1.2 User/relationship associations

屏幕快照 2016-05-31 下午8.32.00.png

12.1.4 Followed users && 12.1.4 Followers

followeds 看起來太丑,于是用following:
followers:自己的粉絲
a). 設置關聯屬性

//app/models/user.rb
class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :active_relationships, class_name:  "Relationship",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy  
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy 
  has_many :following, through: :active_relationships, source: :followed
  has_many :followers, through: :passive_relationships, source: :follower

...

b). 然后我們需要在user model中添加關注方法,就是,真正實現關聯的action

//app/models/user.rb
class User < ActiveRecord::Base
...
  def feed
    Micropost.where("user_id = ?", id)
  end

  # Follows a user.
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

  # Unfollows a user.
  def unfollow(other_user)
    active_relationships.find_by(followed_id: other_user.id).destroy
  end

  # Returns true if the current user is following the other user.
  def following?(other_user)
    following.include?(other_user)
  end
  
  private
...

polen:
如果大家平時測試中出現類似undefined method "xxx"這種錯誤,常見的是2個原因:

  • 對應的model中沒有設置關聯屬性,像如下:
  has_many :following, through: :active_relationships,  source: :followed
  • 對應的model中沒有定義相關方法,像如下:
  def follow(other_user)
    active_relationships.create(followed_id: other_user.id)
  end

12.2 A web interface for following users

12.2.2 Stats and a follow form

a). 路由加一下:

//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 :users
  resources :account_activations, only: [:edit]
  resources :password_resets,     only: [:new, :create, :edit, :update]
  resources :microposts,          only: [:create, :destroy]
  resources :relationships,       only: [:create, :destroy]
...

b). 寫基礎UI

//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>

c). home頁增加follower相關

//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 %>
...

d) .改布局

...
//app/assets/stylesheets/custom.css.scss
.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 */

...

e). 繼續寫幾個基礎空間:

//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 %>
//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 %>
//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 %>

f). 最后show里面完善下:

//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>
    <section class="stats">
      <%= render 'shared/stats' %>
    </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>

12.2.3 Following and followers pages

a). controller 添加following和followers方法

//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

...

b). show的UI可以做起來了

//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.2.4 A working follow button the standard way

a). Relationships

 rails generate controller Relationships

b). 寫create方法和destroy方法

//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

12.2.5 A working follow button with Ajax

ruby on rails 怎么玩Ajax呢:

a). 引入Ajax
ruby中使用form_for后面加個remote: true,就會自動引入Ajax

polen:
深入學習看這里:Ruby on Rails 實戰聖經:Ajax 應用程式

所以我們的代碼是這樣的:


屏幕快照 2016-06-01 上午11.44.08.png

b). RelationshipsController中加入相應js的代碼
首先看這樣一段代碼:

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

polen:

  • 這里的respond_to意思是對不同的請求進行不同的處理(目的是對不同的瀏覽器做兼容---比如有些瀏覽器禁用了JavaScript).
    意思是如果瀏覽器請求的是html,那么我們就xxxx處理;如果請求的是js,就xxxx處理,當然也可以加xml等等...
    可以參考:ActionController::MimeResponds
  • ruby還有個respond_to?, 其實就多加了個問號,但是目的就完全不一樣了,這個是看看對象是否有對應的方法函數.
    可以參考: Confused about 'respond_to' vs 'respond_to?'
    這個和iOS里的respondsToSelector是一樣的:
  • (BOOL)respondsToSelector:(SEL)aSelector;
* `do |format| `這種格式,在ruby里,"| ... |"里的內容表示參數
* 說到do ,額外插一句ruby的for循環,一直忘記說了.
其他大部分語言喜習慣用`for in xxx`的模式,這個在ruby也可以用,但是推薦用` xxx.each do |item|`,這樣更"ruby"一些.
另外,二者的區別是for循環之后,item的值還是在的;但each循環之后,item值當場就被釋放了.
可以參考:[“for” vs “each” in Ruby](http://stackoverflow.com/questions/3294509/for-vs-each-in-ruby)

>```
# way 1
@collection.each do |item|
  # do whatever
end

># way 2
for item in @collection
  # do whatever
end

c). 添加代碼:

# app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_action :logged_in_user

  def create
    # @user = User.find(params[:followed_id])
    @user = User.find(params[:relationship][: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

polen:
糾錯:
關于獲取當前這個@user
這里原文章是@user = User.find(params[:followed_id],但這實際是有問題的.如果直接看服務端log,能看到我們的請求是:

Parameters: {
"utf8"=>"?", 
"relationship"=>{
      "followed_id"=>"4"
   }, 
"commit"=>"Follow"}

可以看到followed_id是在relationship下面的(相當于2個dictionary 包裹起來的),所以我們的解析應該是params[:relationship][:followed_id]
所以代碼應該是:

    @user = User.find(params[:relationship][:followed_id])

服務端log如下:

User.find(params[:relationship][:followed_id])

同時看他們github的代碼這一行也是如此:
railstutorial/sample_app_rails_4
屏幕快照 2016-06-01 下午5.50.25.png

d). 寫最終的js文件(ajax的本質是最終能調用js文件實現局部刷新):


The Ruby JavaScript (RJS)

polen:
這里還是要做個糾正:
文章里js文件結尾是帶分號的";"的,但是實際跑起來還是會報錯的:

ActionView::Template::Error 
(Missing partial user/_unfollow with 
{:locale=>[:en], :formats=>[:js, :html], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. 
Searched in:
...

Missing partial user/_unfollow

就是找不到_unfollow.html.erb
這里的解決方案是 去掉分號,查看了他們的源碼也是如此:
屏幕快照 2016-06-01 下午5.59.08.png

至于為什么?anyone knows?

e). debug的tips:

polen:
這里說個debug的小tip:
因為之前測試的時候,是不是會報錯,出現udefined method等錯誤,所以有時候需要檢查下當前的user或者current_user
這個如果只是調試的話,代碼里加一行<%= debug current_user%>即可,像這樣:

debug @user

看到的結果是這樣子(可以詳細的看到user的debug信息):
屏幕快照 2016-06-01 下午3.58.54.png

12.3 The status feed

讓我們用戶的feed,不只是自己的推文,還有其他用戶的(不就是個"朋友圈"么??).
產品經理說要做成這樣子:


屏幕快照 2016-06-01 下午3.43.12.png

12.3.2 A first feed implementation

第一步,所有following_ids里的我們都添加進來

//app/models/user.rb
...
  def feed
    Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id)
  end
...

12.3.3 Subselects

做個小小的優化,提升sql效率:

//app/models/user.rb
...
  def feed
    following_ids = "SELECT followed_id FROM relationships
                     WHERE  follower_id = :user_id"
    Micropost.where("user_id IN (#{following_ids})
                     OR user_id = :user_id", user_id: id)
  end
...

<br />

Github:


本文所有的代碼已上傳github:
polegithub/rails_sample_app_polen

相關:


[Ruby]《Ruby on Rails Tutorial》的搬運工之一
[Ruby]《Ruby on Rails Tutorial》的搬運工之二

<br />

插一曲:

萬萬沒想到看完這本書的時候,才發現有中文版本,好吧
默默的寫在這里了(感謝安道童鞋的翻譯):
Ruby on Rails 教程: 通過 Rails 學習 Web 開發

<br />


by poles

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容