【Laravel×Vue.js(CDN)いいね機能】DB使用・会員登録なしで、localStorageでいいね(お気に入り)機能を実装する

やりたいこと

データベースを使用しない、簡易的ないいね機能を実装(各ブラウザのlocalStorageを使用する)

今回はキャラクター一覧・詳細画面で、いいねを付ける機能をサンプルとして実装します。

※キャラクターは固定表示で、キャラクターの登録編集削除部分は実装せず、いいねの部分だけになります。

Local Storageとは?

ブラウザ(ChromeやSafariなど)にデータを保存できる仕組み

よくECサイトで、会員登録をしていないのにもかかわらず、「最近閲覧した商品」「お気に入り商品」などが表示されているのはlocalStorageを使用している為。

データベースを使用せず、localStorageを使用するメリット

会員登録をする必要が無い

データベースを使用せず、localStorageを使用するデメリット

localStorageを使用してブラウザに保存する為、キャッシュクリアや、別端末、別ブラウザで閲覧されるとお気に入りを共有できない

実装内容詳細

  1. お気に入りの状態によって、表示するアイコンの色を変える
  2. 画面遷移をしても、お気に入りの状態が正しい事(ブラウザバック・一覧へ戻るボタン押下)

バージョン

Laravel:10.3.3

Vue:2.6.10

デモ

Vue.js

LaravelでVue.jsを取り入れる方法は、主に以下の2種類の使い方が可能です。

今回は、CDNを読み込む方法で実装します。

CLIツール

ビルドの複雑な処理の一部 (Babel や Webpack の使用など) を実行する、

グローバルにインストールされる npm パッケージ

Node.js、ソースのビルドが必要です。

Vue.jsの記事でよく見るexport defaultは、このCLIツールを使用しています。

CDN

CDNとは以下のような形で外部から読み込んで使う形の事です。

Vue CLIより簡単に使用することが出来、使いやすいと思います。

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>

サンプルソース

githubの「localstorage-vue-cdn」ブランチにアップしています。

大まかな流れ

  1. お気に入りのvueコンポーネントを作る
  2. 一覧画面・詳細画面を用意
  3. 表示するデータを用意(今回は固定指定なので、各画面に表示したいデータをべた書き)

1)お気に入りのvueコンポーネントを作る

フォルダ構成はgitのソースをご確認ください。

お気に入りコンポーネント(Vue.js)

jsLikeButtonクラスの要素を確認してもらえれば、中身を設定していないことが分かりますが、

これは要素のクラス名によってcssで表示するアイコン(未いいね、いいね済)を変更しています。

<template>
    <div class="jsLikeButton" :class="className" @click="setLike($event)">
        <!-- いいねアイコンはcssで設定。クラス名によって表示を変更 -->
    </div>
</template>

<script>
    module.exports = {
        delimiters: ['(%', '%)'], // Vue.js のデータバインディングは二重中括弧で blade と重複するため変更
        data: function() {
            return {
                likes: [],
                className: 'unLike'
            }
        },
        created: function () {
            // 画面表示の度にお気に入りの状態を更新
            window.addEventListener('pageshow', this.init);
        },
        beforeDestroy() {
            window.removeEventListener('pageshow', this.init);
        },
        props: ['id'],
        methods: {
            init() {
                // お気に入りを取得して変数に保持
                this.likes = this.getLikes();
                this.setClassName();
            },
            getLikes() {
                let values = [];
                let localStorageValues = JSON.parse(localStorage.getItem('likes'));
                if (localStorageValues) {
                    values = localStorageValues;
                }
                return values;
            },
            isLiked() {
                let indexPosition = this.likedIndexPosition();
                // 含まれている
                if (this.likes && -1 < indexPosition) {
                    return true;
                }
                // 含まれていない
                return false;
            },
            likedIndexPosition() {
                // 配列の何番目のインデックスに含まれているか(配列中にマッチする値が含まれなければ-1を返す)
                return this.likes.indexOf(this.id);
            },
            setClassName() {
                // 既にお気に入りか
                if (this.isLiked()) {
                    this.className = 'liked';
                } else {
                    this.className = 'unLike';
                }
            },
            setLike(event) {
                if (event) {
                    event.preventDefault();
                }
                // localStorageが存在している
                if (this.likes) {
                    // 既にお気に入り
                    if (this.isLiked()) {
                        let indexPosition = this.likedIndexPosition();
                        // お気に入りから該当idを除去
                        this.likes.splice(indexPosition, 1) ;
                    } else {
                        // お気に入り追加
                        this.likes.push(this.id);
                    }

                } else {
                    // localStorageが存在していない場合
                    // お気に入り追加
                    this.likes.push(this.id);
                }

                // 新しい値をlocalStorageに設定
                localStorage.setItem('likes', JSON.stringify(this.likes));
                // 表示の更新
                this.setClassName();
            },
        }
    }
</script>

<style scoped>
.jsLikeButton {
    width: 50px;
    height: 50px;
}
.jsLikeButton.unLike {
    background-image: url(画像パス);
}
.jsLikeButton.liked {
    background-image: url(画像パス);
}
.jsLikeButton img {
    width: 100%;
}
</style>

window.addEventListener('pageshow', this.init);としていますが、ブラウザの戻るボタンで戻った際に、一覧画面や詳細画面のお気に入りの状態が古い状態のままになってしまうのを防ぐためです。

今回はpageshowイベントを使用しましたが、よく他の記事ではwindow.addEventListener('popstate', () => {が使用されています。popstateだと、Chromeではセキュリティの問題で表示した各画面でクリックなど、ユーザーによる1アクションが無ければ発火しません。(※各ブラウザによっても動きが異なる可能性があります)

その為、今回はpageshowイベントを使用しました。ブラウザバック含めた画面を表示するタイミングで発火されるので、重い処理を何回もしていれば遅くなってしまうかもしれませんが。。

indexお気に入りコンポーネントの読み込み)

お気に入りコンポーネントを、一覧画面・詳細画面と使いまわすので、コンポーネント化したく、その為にhttp-vue-loaderというライブラリを使用しました。

Vue CDNを読み込む場合も、コンポーネント化はできるようですが、いろいろやり方があるようです。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex,nofollow">

    <title>{{ config('app.name') }}</title>

    <!-- Fonts -->
    <link href="https://use.fontawesome.com/releases/v5.6.1/css/all.css" rel="stylesheet">

    <!-- Styles -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    <link href="{{ asset('css/common.css') }}" rel="stylesheet">

    <!-- Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>
</head>
<body>
    <div id="app">
        @yield('content')
    </div>

    <!-- Scripts -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
    <script type="text/javascript">
        new Vue({
            el: '#app',
            data() {
                return {
                };
            },
            components: {
                'like': httpVueLoader('{{ url('components/like.vue') }}'),
            },
        });
    </script>
</body>
</html>

以下の部分で、http-vue-loaderを使用しています。likeというコンポーネントで登録しているので、他ソースでは<like></like>という形で使用が可能です。

詳細な使い方は参考サイトに記載があります。

    <!-- Vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/http-vue-loader"></script>


    <script type="text/javascript">
        new Vue({
            el: '#app',
            data() {
                return {
                };
            },
            components: {
                'like': httpVueLoader('{{ url('components/like.vue') }}'),
            },
        });
    </script>

2)一覧画面・詳細画面を用意

作成したお気に入りコンポーネントを、一覧画面・詳細画面で<like></like>タグで読み込んでいます。

お気に入りコンポーネントのvueの処理中に、idを使用するので:id="{{ $item['id'] }}"でidを渡しています。

一覧画面

// 一覧画面
<div class="container mt-5">
    <div class="row">
        @foreach($items as $item)
            <a href="/{{ $item['id'] }}" class="col-12 col-md-6 mb-3">
                <div class="card characterCard p-4">
                    <!-- いいねアイコン -->
                    <like :id="{{ $item['id'] }}"></like>

                    <div class="d-flex mt-2">
                        <!-- 画像 -->
                        <div>
                            <img src="{{ asset('img/character/'.$item['image']) }}" alt="{{ $item['name'] }}">
                        </div>

                        <!-- 説明 -->
                        <div class="ps-3">
                            <h2>{{ $item['name'] }}</h2>
                            <p class="mt-2 text-sm text-muted">
                                {!! $item['description'] !!}
                            </p>
                        </div>
                    </div>
                </div>
            </a>
        @endforeach
    </div>
</div>

詳細画面

// 詳細画面
<div class="container">
    @if ($item)
        <div class="mt-5 characterDetail">
            <!-- いいねアイコン -->
            <like :id="{{ $item['id'] }}"></like>

            <!-- 画像 -->
            <div class="text-center">
                <img src="{{ asset('img/character/'.$item['image']) }}" alt="{{ $item['name'] }}">
            </div>
            <!-- 説明 -->
            <div class="card mt-3 p-4">
                <h2>{{ $item['name'] }}</h2>
                <p class="mt-2 text-sm text-muted">
                    {!! $item['description'] !!}
                </p>
            </div>
        </div>
    @else
        <!-- 該当するキャラクター無し -->
        <div class="mt-5 text-center">
            <p>該当するキャラクターがありませんでした</p>
        </div>
    @endif

    <a href="/" class="backBtn">一覧に戻る</a>
</div>

2)表示するデータを用意(今回は固定指定なので、各画面に表示したいデータをべた書き)

// 表示するデータ
$items = [
    ['id' => 1, 'name' => 'ひよこ隊長', 'image' => 'hiyoko.png', 'description' => '夏が大好きなひよこ隊長。<br>夏はよくフェスに行き、夏を堪能している。'],
    ['id' => 2, 'name' => 'フクロウ先生', 'image' => 'fukurou-sensei.png', 'description' => '森の学校のフクロウ先生。<br>最近物忘れすることを本人は気にしている。'],
    ['id' => 3, 'name' => 'パンダ氏', 'image' => 'panda.png', 'description' => 'パンダ財閥の次男。<br>蝶ネクタイがトレードマーク。'],
    ['id' => 4, 'name' => 'おしゃべりオウム', 'image' => 'oumu.png', 'description' => 'おしゃべり大好きオウム君。<br>おしゃべりしすぎて、フクロウ先生に怒られる事が多い。'],
];

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です