LoginSignup
2
2

More than 1 year has passed since last update.

Rails × Vue.jsでいいね機能を実装

Last updated at Posted at 2021-08-24

ほぼほぼ、この方の記事のまんまです。ただ自分なりの実装方法もあったのでそれを今回は解説していきたいと思います。

user
post
like

モデルを使います。僕の場合はpostがdrinkになってるので適宜読み替えてください。

アソシエーションについては、いいね機能でよくあるやつです。
非同期じゃなくていいので、まずはrails単体でいいね機能を実装してから、この記事に取り掛かることをお勧めします。

あと、普通に素人解説なので、きっと間違えてます。

コントローラー の作成

APIとしてのlikes controllerを作成します。
ターミナル上で以下を実行します。apiという名前空間を切っています。またAPIなのでassets等、不要なものは省いておきます。

rails g controller api/likes index create destroy --skip-template-engine --skip-test-framework --skip-assets --skip-helper

app/controllers/api/likes_controller.rb

class Api::LikesController < ApplicationController

  before_action :set_variables

  def index
    # その投稿のいいね一覧を取得
    render json: Like.filter_by_drink(params[:drink_id]).select(:id, :user_id, :drink_id)
  end

  def create
    @like = @user.likes.new(drink_id: @drink.id)
    @like.save

    head :created
  end

  def destroy
     @like = @user.likes.find_by(drink_id: @drink.id)
     @like.destroy
    head :ok
  end

  private

  def set_variables

    @user = User.find(params[:user_id])
    @drink = Drink.find(params[:drink_id])
  end

  def likes_params
    params.require(:like).permit(:drink_id)
  end

end

headとは、ステータスコードを返すメソッドです。
Vue.jsの方でRails APIによって返されたステータスコードによって
処理が変わってくるので、あとで見て見ましょう。

models/like.rb

models/like.rb

like.rb

class Like < ApplicationRecord
  belongs_to :user
  belongs_to :drink, counter_cache: :likes_count
# デフォルトでは親モデル_countに値が入るが、ここではカラム名を指定してる
# 取得したN件のdrinkに対して,一つひとつcountクエリが実行されないように設定
# N+1問題を解決するためのオプション
# これの上位互換のcounter_cultureといったgemもあるから
# 余裕があればやっとこう


  scope :filter_by_drink, ->(drink_id) { where(drink_id: drink_id) if drink_id }
end

scopeとはクラスメソッド を定義するメソッドです。
defでもクラスメソッド を定義できますが、一行で書けるのが特徴。

クラスメソッド とは、クラスオブジェクトから実行可能なメソッドです。
https://qiita.com/right1121/items/c74d350bab32113d4f3d

Like.filter_by_drinkという風にできます。
パラメーターで受け取ったdrink_idを元に、それと一致するものをLikes Tableから取り出してくれるメソッド。
つまり、特定の投稿についてる全ていいねを一挙に取得するメソッド。

route.rb

routes.rb

  namespace :api, { format: 'json' } do
    resources :likes, only: [:index, :create, :destroy]
  end

namespace :apiと書くことで、作られるパスが自動的に
api_create_pathとか、api/create/drink_idとかになってくれます。
コントローラーもapi::likes_controller#indexを自動的に参照というか、反応するようになってます。

<%= javascript_pack_tag 'hello_vue' %>

を任意のerbファイルに記述してください。そこにapp.vueが表示されるようになります。

_like.html.erb

_like.html.erb


<div class="like" id="like-link-<%= drink.id %>" hidden>
  <% if current_user.likes.find_by(drink_id: drink.id) %>
    <%= link_to unlike_path(drink.id), method: :delete, remote: true do %>
        <div class = "iine__button">
          <i class="fas fa-heart"></i>
          <%= drink.likes.count %>
        </div>
    <% end %>
  <% else %>
    <%= link_to like_path(drink.id), method: :post, remote: true do %>
        <div class = "iine__button">
          <i class="far fa-heart">
          </i><%= drink.likes.count %>
        </div>
    <% end %>
  <% end %>
</div>

<script>
var user = <%= { id: current_user.try(:id) }.to_json.html_safe %>;
var drink = <%= { id: drink.try(:id) }.to_json.html_safe %>;
</script>

こんな感じで記述してください。


<div class="like" id="like-link-<%= drink.id %>" hidden>

元々railsでいいね機能を実装してた時には必要でしたが、表示する必要がないのでhiddenとしてください。

hiddenじゃなくて、単純に消せばいいんじゃね。とお思いでしょうが、以下の記述をご覧ください。

このように記述することで、current_userのidや、drink.idをVueにjsonデータとして渡せます。
記述する理由は、それらのid情報を元に、vueの方でRailsの方にリクエストを送りたいからです。
どのようなリクエストかというと、api::likes#index,create,destroyを動かすためのリクエストです。

_like.html.erbのcurrent_user.idをそのまま使えるのは便利ですね。

ただこれらは、drinks#showでdrinkを定義して,drinks/show.html.erbでパーシャル として
_like.html.erbを切り出していたりするので、いつかvue-routerを使って、
あとで作るlikeButton.vueを表示させようとしたら間違いなくうまく行かない気がします。
こんなクソコードを書くと技術的負債を後任のエンジニアに押し付けることができます!!
やったね!!!

app.vue

likeButton.vueを
app/javascript/packs/components/like/likeButton.vue
でファイル作成して

app.vue

<template>
  <div>
   <likeButton></likeButton>
  </div>
</template>

<script>
import likeButton from './packs/components/like/likeButton.vue'
</script>

こんな感じで読み込む。

likeButton.vue

いよいよ大台のlikeButton.vue。

likeButton.vue

<template>
  <div>
    <div v-if="isLiked" @click="deleteLike()">
      <div class="iine__button">
        <i class="fas fa-heart"></i> {{ count }}
      </div>

    </div>
    <div v-else @click="registerLike()">
      <div class="iine__button">
        <i class="far fa-heart"></i> {{ count }}
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
// import { csrfToken } from 'rails-ujs'
// // CSRFトークンの取得とリクエストヘッダへの設定をしてくれます
// axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken()



export default {
   props: ['user', 'drink'],
   data(){
     return{
       likeList: []
       // いいね一覧を格納するための変数
     }
   },
   computed: {
     // データが変更されるたび動く
     // ここではlikeListが変更される度に、count,isLikedが再構築される
     count(){
       return this.likeList.length
       // いいね数を返す
     },
     isLiked(){
       // ログインユーザーが既にいいねしてるかを判定する
       if (this.likeList.length === 0){ return false}
            return Boolean(this.findLikeId())
     }
   },
   created: function(){
     // vueインスタンスの作成、初期化直後に実行される
     this.fetchLikeByDrinkId().then(result =>{
       this.likeList = result
     })
   },
   methods: {
    fetchLikeByDrinkId: async function(){
       // async function()
       // jsの非同期処理
        const response = await axios.get('/api/likes',{params: {drink_id:drink.id,user_id: user.id}})
        // await 
        // その投稿のいいね一覧を取得したい
        if (response.status !== 200){ process.exit()}
        // もし処理が失敗したらプロセスから抜ける(処理をやめる?)
        return response.data

    },
     registerLike: async function(){
       // rails側のcreateアクションにリクエストするメソッド
       const response = await axios.post('/api/likes',{drink_id: drink.id,user_id: user.id})
       if (response.status !== 201) {process.exit()}
       this.fetchLikeByDrinkId().then(result => {
        this.likeList = result

       })
     },
     deleteLike: async function(){
       // rails側のdestroyアクションにリクエストするメソッド
       const likeId = this.findLikeId()
       const response = await axios.delete(`/api/likes/${likeId}`,{params: {drink_id:drink.id,user_id: user.id}})
       if (response.status !== 200){process.exit()}
       this.likeList = this.likeList.filter(n => n.id !== likeId)
     },
     // ログインユーザーがいいねしているLikeモデルのidを返す
     findLikeId: function(){
       const like = this.likeList.find((like) => {
         return (like.user_id == user.id)
       })
       if (like) { return like.id }
     }
   }
}
</script>

import axios from 'axios'

axiosは非同期通信を可能にするモジュールです。


   props: ['user', 'drink'],

は、さっきの

_like.html.erb

<script>
var user = <%= { id: current_user.try(:id) }.to_json.html_safe %>;
var drink = <%= { id: drink.try(:id) }.to_json.html_safe %>;
</script>

のvar user, var drinkの受け取り口的な感じです。

これら2つの組み合わせで、likeButton.vueでdirnk.idとか、user.idとかやれば
現在ログインしてるユーザーのidと、今見てる投稿のidを取得できます。

isLiked()


     isLiked(){
       // ログインユーザーが既にいいねしてるかを判定する
       if (this.likeList.length === 0){ return false}
            return Boolean(this.findLikeId())
     }

一番目の引数に渡された値は、必要に応じて論理値に変換されます。値が省略された場合や、値が 0, -0, null, false, NaN, undefined あるいは空文字列 ("") であった場合、オブジェクトは false の初期値を持ちます。それ以外のあらゆる値は、オブジェクトや "false" という文字列も含めて、 true の初期値を持つオブジェクトを生成します。

ってことでtrueを返してくれます。

likeButton.vue
    <div v-if="isLiked" @click="deleteLike()">

ここがtrueになってv-ifの条件分岐が機能する。って感じ。

return Boolean(this.findLikeId())

findLikeId()の説明

ここで登場したfindLikeId()関数を見とくと

likeButton.vue.js
     // ログインユーザーがいいねしているLikeモデルのidを返す
     findLikeId: function(){
       const like = this.likeList.find((like) => {
         return (like.user_id == user.id)
       })
       if (like) { return like.id }
     }

.find()は配列につかうメソッドで、
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find

find() メソッドは、提供されたテスト関数を満たす配列内の 最初の要素 の 値 を返します。

なぜ上記のようなコードになっているかもう一度背景を説明すると

likeListは色々なユーザーIDが入っています。

     isLiked(){
       // ログインユーザーが既にいいねしてるかを判定する
       if (this.likeList.length === 0){ return false}
            return Boolean(this.findLikeId())
     }

ログインユーザーがすでにいいねしてるかを確かめるために
その投稿についてるいいね一覧を取得して、その中にログインしてるユーザーのidがあった時点で
その投稿に、ユーザーはいいねをしてるということになります。

なので、

     // ログインユーザーがいいねしているLikeモデルのidを返す
     findLikeId: function(){
       const like = this.likeList.find((like) => {
         return (like.user_id == user.id)
       })
       if (like) { return like.id }
     }

likeList,その投稿についてるいいねの全部の情報の配列にfind()を実行して
like,つまりその投稿にどんなユーザーがいいねしたか。
こんかいは、drink_idは1でuser_idが1だった場合、
いいね情報のuser_idと現在ログインしてるユーザーのidが一致したらその要素を返す。
つまり、一つのいいね情報を返す。
もし、すでにユーザーがいいねしてたら、言い換えると、いいね情報のuser_idとログインしてるユーザーのidが一致したら、そのlike.idを返す。いいね情報のidを返す。

user.idで現在ログインしてるユーザーのidが取得できる仕組みは上記で説明した通りです。

created: function()

   created: function(){
     // vueインスタンスの作成、初期化直後に実行される
     this.fetchLikeByDrinkId().then(result =>{
       this.likeList = result
     })
   },

created: function()
の中に定義した関数を記述すると、その関数はvueインスタンスの作成、初期化直後に実行されます。

何が実行されるかと言うと、これらのコードが実行されます。

     this.fetchLikeByDrinkId().then(result =>{
       this.likeList = result
     })

ただ、まずはfetchLikeByDrinkId()を見ましょう。
あとで戻ってきます。

fetchLikeByDrinkId()

    fetchLikeByDrinkId: async function(){
       // async function()
       // jsの非同期処理
        const response = await axios.get('/api/likes',{params: {drink_id:drink.id,user_id: user.id}})
        // await 
        // その投稿のいいね一覧を取得したい
        if (response.status !== 200){ process.exit()}
        // もし処理が失敗したらプロセスから抜ける(処理をやめる?)
        return response.data

    },

async function()は非同期処理の宣言文です。
非同期処理とは、時間の掛かりそうな処理があった場合、次の処理を一旦実行して、あとで時間がかかった処理の結果を返す。的なやつです。

食券の例えが僕は好きなのですが、通常の処理だと、食券買って、食券を店員に渡して、料理ができるまでその場で待たなくてななりません。
非同期処理だと、食券を店員に渡して、自分はテーブルに戻ってゲームして待って、その間に厨房が料理を作って、
できたら読んでもらうとか、そんな感じ
ただ、なぜlikebutton.vueで非同期通信をつかったほうがいいのかわからん。。
```js

const response = await axios.get('/api/likes',{params: {drink_id:drink.id,user_id: user.id}})
```

axiosは非同期通信をするモジュールで、axios.HTTP動詞(URL)とすると、引数のURLに非同期通信をしてくれます。

その後はrailsの世界で、route.rbにリクエストがきて、それに対応するアクションが動きます。
railsの法でparamsの処理があるので、パラメーターを以上のように渡しときます。
ここでもdrink.id、user.idを使います。

今回は、api::likes#indexの処理が動きます。

余談ですが、process.exit()は多分node.jsのメソッドなので使えないです。
削除しましょう。

if (response.status !== 200){ process.exit()}
は、

api/likes_controller.rb


  def index
    # その投稿のいいね一覧を取得
    render json: Like.filter_by_drink(params[:drink_id]).select(:id, :user_id, :drink_id)
  end


```like.rb

  scope :filter_by_drink, ->(drink_id) { where(drink_id: drink_id) if drink_id }

vueからパラメーターとして、渡されたdrink_idをもとに、
Likesテーブルからデータを取得して、それをjson形式でVueに返してます。

 return response.data

として、返されます。

   created: function(){
     // vueインスタンスの作成、初期化直後に実行される
     this.fetchLikeByDrinkId().then(result =>{
       this.likeList = result
     })
   },

そして、さっきの created: function()に戻ると、
async function()の特徴の一つでthenは、fetchLikeByDrinkId()が終わったら実行されます。
resultはresponse.dataと一緒です。

deleteLike: async function()

     deleteLike: async function(){
       // rails側のdestroyアクションにリクエストするメソッド
       const likeId = this.findLikeId()
       const response = await axios.delete(`/api/likes/${likeId}`,{params: {drink_id:drink.id,user_id: user.id}})
       if (response.status !== 200){process.exit()}
       this.likeList = this.likeList.filter(n => n.id !== likeId)
     },

最後、 this.likeList = this.likeList.filter(n => n.id !== likeId)
について説明します。

nはlikeListの配列一つ一つです。
そのidが、likeIdと一致しないものを取り出して、新たな配列likeListを作ります。
つまり、userがいいねしたらそれを削除する感じです。

最後に

railsのerbからvueの方にデータを受け渡したりしてるので、root_pathからview_routerとかで表示させようとしたら上手くいくのか疑問です。。。
_like.html.erbを介さないと上手く動かないしようになってるので、railsからvueへのデータの受け渡しをもう少し工夫する必要がありそうなので、まだまだ完璧ではありあません。
あと、まだまだasyncの理解が乏しいので今からasyncの勉強をしたいと思います。

また、分かりづらいとか、なにか質問があればコメントや、twitterまで気軽にお願いします

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2