[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 href="http://www.railstutorial.org/">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阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容