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

Part.1 では犬種リストの表示まで実装しました。今回は画像表示から「いいね!」ボタン、ページネーションの実装までを行いアプリを完成させます。

過去の記事は以下から。
Nuxt.js で簡単な画像一覧アプリを作成する – Part.1

構成について

特定の犬種をクリックした際、画像が一覧表示されるようにします。エンドポイントは 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です。

画像を表示する

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>

ページングの追加

時間の都合上、勉強会の内容はここで終了となりましたが今回はページングまで実装してみます。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>

参考になるドキュメントを以下にまとめておきます。

validateNuxt.js で動的ルーティングを行う際、値を検証するために使います。ドキュメントは  API: validate メソッド を参照。
nuxt-linkrouter-link のラッパーなので、 router-link を参照。
watchQueryクエリストリングの変更を検知します。ページングボタン押下時にコンテンツを再描画するため必須です。ドキュメントは  API: The watchQuery Property を参照。
asyncDataNuxt.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 にアクセス