LuftGarden - 熊本のWebサイト・ホームページ制作スタジオ

BLOG技術ブログ

Nuxt.js で簡単な画像一覧アプリを作成する – Part.2

はじめに

Nuxt.js で簡単な画像一覧アプリを作成する – Part.1」の続きになります。

Part.1 ではNuxt.jsプロジェクトの作成〜犬種リストの表示までを実装しました。
今回はイヌの画像表示から「いいね!」ボタンの実装までを行い、アプリを完成させましょう。

構成について

特定の犬種をクリックした際、画像が一覧表示されるようにします。
エンドポイントは https://dog.ceo/api/breed/[犬種名]/images です。詳細は Dog APIのドキュメント を参照してください。

画像一覧表示のルーティング

ルーティング(ディレクトリ構成)は上記のようになります。
ディレクトリ名がアンダースコア付きで _breed となっているので、犬種は動的に設定できます。

画像取得のメソッドを追加する

それでは、さっそく開発を進めていきましょう。
イヌの画像を取得するメソッドをDogApiクラスに追加します。

api/dog.js
class DogApi {
    // ...中略...

    dogs( breed ) {
        return axios.get(`${this.apiBase}/breed/${breed}/images`)
            .then(json => {
                return json.data.message.map( (d) => {
                    return {
                        url: d,  // 画像URL
                        like: 0, // 「いいね!」の件数
                    };
                });
            })
            .catch(e => ({ error: e }));
    }
}

取得したデータを保存できるように、state と mutation も追加しておきます。

store/index.js
const appStore = () => {
    return new Vuex.Store({
        state: {
            breed_list: {},
            dog_list: {},  // 追加
        },
        mutations: {
            breed_list_update(state, payload) {
                state.breed_list = {...payload}
            },

            // 追加
            dog_list_update(state, payload) {
                state.dog_list = [...payload]
            },
        }
    })
};

画像一覧ページの追加

pages/dogs/_breed/index.vueを追加し、 fetch() で画像取得のAPIを叩くようにします。

pages/dogs/_breed/index.vue
<template>
    <section class="container">
        <div class="columns is-multiline">
        </div>
    </section>
</template>

<script>
    import dogApi from '@/api/dog';
    import {mapState} from 'vuex';

    export default {
        async fetch({store, params}) {
            const json = await dogApi.dogs(params.breed);
            store.commit('dog_list_update', json)
        },
        computed: mapState(['dog_list']),
    }
</script>

犬種をクリックした際に、画像一覧ページへ遷移するようリンクを修正します。

pages/index.vue
<template>
    <section class="container">
        <div class="columns is-multiline">
            <div v-for="(item, i) in breed_list" v-bind:key='i' class='column is-2'>
                <!-- <a class="button">{{ i }}</a> -->
                <nuxt-link :to="{ path: 'dogs/'+ i }" class="button">{{ i }}</nuxt-link>
            </div>
        </div>
    </section>
</template>

犬種をクリックして画面遷移後、Vue.js devtools で dog_list にデータが入っていることが確認できればOKです。

dog_listストアにデータが入っている状態の画面キャプチャ

画像を表示する

APIで取得したデータをもとに画像表示します。

pages/dogs/_breed/index.vue
<template>
    <section class="container">
        <div class="columns is-multiline">

            <!-- 追加 -->
            <div v-for="(item, i) in dog_list" v-bind:key="i" class="column is-1">
                <img :src="item.url">
            </div>
        </div>
    </section>
</template>

犬種を選択した際、以下のように画像表示されればOKです。

コリー犬の画像がずらっと並んだ画面キャプチャ

「NEW」ラベルと「いいね!」ボタンを配置する

イヌの画像と合わせて「いいね!」ボタンを配置するようにページを改修します。
先頭3件には「NEW」ラベルも表示されるようにしましょう。

pages/_breed/index.vue
<template>
    <section class="container">
        <div class="columns is-multiline">
            <div v-for="(item, i) in dog_list" v-bind:key="i" class="column is-2">
                <img v-bind:src="item.url">

                <!-- 追加 -->
                <span v-if="i < 3" class="tag is-danger">NEW</span>
                <a class="button is-warning  is-small" v-on:click="item.like += 1">
                    <span>いいね!{{item.like}}件</span>
                </a>
            </div>
        </div>
    </section>
</template>

イヌの画像下部に「NEW」ラベルと「いいね!」ボタンを表示した状態の画面

これで機能要件はすべて実装することができました。

ページングの追加

時間の都合上、勉強会の内容はここで終了となりましたが今回はページングまで実装してみます。
Dog API側でページング処理を行うことはできないようなので、データとしては全件取得し、フロント側でデータをsliceすることでページングします。

まずは、ページ件数を保持するために store を追加します。

store/index.js
const appStore = () => {
    return new Vuex.Store({
        state: {
            breed_list: {},
            dog_list: [],
            page_count: 1,  // 追加
        },
        mutations: {
            breed_list_update(state, payload) {
                state.breed_list = {...payload};
            },
            dog_list_update(state, payload) {
                state.dog_list = [...payload];
            },

                        // 追加
            page_count_update(state, payload) {
                state.page_count = parseInt( payload );
            },
        }
    })
};

store の準備ができたら、画像一覧のページにページング用のコンポーネントと処理を追加します。

クエリストリングを使い、?page=1 という感じでページング処理を行うようにします。
Nuxt.js では、 コンテキストcontext.query を参照してクエリストリングを受け取ります。

pages/dogs/_breed/index.vue
<template>
    <section class="container">
        <div class="columns is-multiline">
            <div v-for="(item, i) in dog_list" v-bind:key="i" class="column is-2">
                <img v-bind:src="item.url">

                <span v-if="i < 3" class="tag is-danger">NEW</span>
                <a class="button is-warning  is-small" v-on:click="item.like += 1">
                    <span>いいね!{{item.like}}件</span>
                </a>
            </div>
        </div>

        <nav class="pagination" role="navigation" aria-label="pagination">
            <ul class="pagination-list">
                <li v-for="count in page_count" :key="count">
                    <nuxt-link class="pagination-link"
                               :class="{ 'is-current' : current == count }"
                               :to="{ path: '?page=' + count }" append>
                        {{ count }}
                    </nuxt-link>
                </li>
            </ul>
        </nav>
    </section>
</template>

<script>
    import dogApi from '@/api/dog';
    import {mapState} from 'vuex';

    export default {
        watchQuery: [
            'page'
        ],
        validate ({ params }) {
            return /^[a-z]+$/.test( params.breed );
        },
        data: function() {
            return {
                current: 1,
            };
        },
        asyncData: function( context ) {
            return {
                current: parseInt( context.query['page'] ) || 1,
            }
        },
        fetch: async function( {store, params, query} ) {
            const page = parseInt( query['page'] ) || 1;
            const start = 20 * ( page - 1 );
            const end = start + 20;

            const json = await dogApi.dogs( params.breed );

            store.commit( 'page_count_update', Math.ceil( json.length / 20 ) );
            store.commit( 'dog_list_update', json.slice( start, end ) );
        },
        computed: mapState([
            'page_count',
            'dog_list',
        ]),
    }
</script>

1つずつ解説していくとかなり時間がかかりますので、参考になるドキュメントを以下にまとめておきます。

validate Nuxt.js で動的ルーティングを行う際、値を検証するために使います。ドキュメントは  API: validate メソッド を参照。
nuxt-link router-link のラッパーなので、 router-link を参照。
watchQuery クエリストリングの変更を検知します。ページングボタン押下時にコンテンツを再描画するため必須です。ドキュメントは  API: The watchQuery Property を参照。
asyncData Nuxt.js のコンテキストを Vueコンポーネントのデータオブジェクトに同期します。ドキュメントは  API: asyncData メソッド を参照。

ページング用のコンポーネントについては  Bulmaのドキュメントを参照してください。

ページネーションを追加した画面のキャプチャ

静的ファイルとしてデプロイする

Nuxt.js のデプロイ方法は多様ですが、今回は一番シンプルな静的ファイルでデプロイしてみます。
といっても、コマンドで npm run generate を叩くだけです。
デフォルトでは dist ディレクトリに静的ファイルが生成されます。

$ cd path/to/nuxt/project
$ npm run generate
$ cd ./dist
$ php -S localhost:28080  // http://localhost:28080 にアクセス

おわりに

Nuxt.js での開発いかがだったでしょうか。実際に使ってみると Vue.js で開発を行う際に一通り必要な機能が揃っており、ルーティングや状態管理がとても簡単に実装できる点はとても便利だと感じました。

引き続き勉強されたい方は、Headless CMS の Contentful と Nuxt.js を組み合わせたSPA構築なんかに挑戦してみてはいかがでしょうか。

PAGETOP