4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

初心者必見!初めてのAPI通信〜Nuxt.js編〜

4
Last updated at Posted at 2020-04-04

初心者必見!初めてのAPI通信〜Nuxt.js編〜

Nuxt.jsでQiitaのAPIを叩いて、情報を取得して表示させるアプリを実装したので
共有します。

成果物

ssr.png

環境

・Nuxtプロジェクト作成

$ npx create-nuxt-app <project-name>

・axios-moduleの利用
axiosはAPI通信するための拡張モジュールです。

$ npm install --save @nuxtjs/axios

・axios-moduleをインストールしたらnuxt.config.jsにaxiosの末尾に下記を追加します

nuxt.config.js
export default {
  // ......省略
  build: {
    // ......省略
  },

  // ここから追加
  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
  }
}

Qiitaのアクセストークンを発行

Qiitaトークン発行URL
https://qiita.com/settings/tokens/new

スクリーンショット 2020-04-05 1.39.08.png

アクセストークンの説明を任意で入れる
スコープはread_qiitaのみでOK

(注)
発行ボタンをクリックすると、アクセストークンは発行されるので
必ず、メモしておいてください。
セキュリティ上、画面を閉じると、その画面に戻れません。

認証情報の設定

plugins/axios.js
export default function({ $axios }) {
    $axios.onRequest((config) => {
        if(process.env.QIITA_TOKEN) {
            config.headers.common['Authorization'] = process.env.QIITA_TOKEN;
        }
        return config
    });
}

プラグインは1つの関数を返す
axios-moduleのインスタンスである$axiosが格納されている

nuxt.config.jsに追加

nuxt.config.js
export default {
  // ......省略
  build: {
    // ......省略
  },

  modules: [
    '@nuxtjs/axios'
  ],
  axios: {
  },

  // ここから追加
  plugins: [
    '~/plugins/axios.js'
  ],
  env: {
    QIITA_TOKEN: process.env.QIITA_TOKEN
  }
}

環境変数を渡す

デフォルトではマシンの環境変数をアプリケーションに渡すことがありません。
もし渡したい場合は下記のように渡しあげる必要があります。

$ export QIITA_TOKEN=<先ほど取得したトークン>
# サーバーを落とし、再起動
$ npm run dev

これでAPIが取得できる状態になりました。

もしくは
.envrcの作成

.envrc

export QIITA_TOKEN=<先ほど取得したトークン>
# サーバーを落とし、再起動
$ npm run dev

非同期通信を含むサーバーサイドレンダリング

pages/index.vue
<template>
  <section class="container">
    <h1><strong>初心者必見!</strong> 初めてのAPI通信 ~Nuxt.js編~</h1>
    <h2>[ Nuxt ] 投稿一覧</h2>
    <ul class="item-lists">
      <li class="item-list" v-for="item in items" :key="item.id">
        <h3>
          <span><a :href="item.url" target="_blank">{{ item.title }}</a></span>
          <p>by {{ item.user.id }}</p>
        </h3>
        <h4>{{ item.body.slice(0, 100) }}.....</h4>
      </li>
    </ul>
  </section>
</template>

<script>
export default {
  async asyncData({ app }) {
    // QiitaのAPIからnuxt.jsのタグが入った情報をゲットしてitemsに格納 => return => あとはtemplate側で表示
    const items = await app.$axios.$get('https://qiita.com/api/v2/items?query=tag:nuxt.js');
    console.log(items);
    return {
      items
    }
  }
}
</script>

<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }
  body {
    border-top: 5px solid rgb(0, 199, 0);
    padding: 0 0 100px 0;
    background: #efefef;
  }
  a {
    color: crimson;
  }
  ul li {
    list-style: none;
  }
  h1 {
    text-align: center;
    font-size: 35px;
    line-height: 1.25em;
    margin: 0 0 50px 0;
    padding: 0 0 5px 0;
    transform: rotate(-5deg);
    color: rgb(0, 199, 0);
    border-bottom: 2px solid rgb(0, 199, 0);
    right: 70px;
    position: relative;
  }
  h2 {
    font-size: 20px;
    font-weight: 500;
  }
  .container {
    width: 60%;
    margin: 50px auto;
  }
  .item-lists {
    margin: 15px 0 0 0;
  }
  .item-lists .item-list {
    padding: 25px 30px 15px;
    border-bottom: 1px solid #ccc;
    background: #fff;
  }
  .item-lists .item-list:last-child {
    border-bottom: none;
  }
  .item-lists .item-list h3 {
    font-size: 18px;
    font-weight: 500;
    line-height: 1.5em;
    letter-spacing: -0.25px;
    margin: 0 0 10px 0;
  }
  .item-lists .item-list h3 p {
    font-size: 12px;
    font-weight: 100;
  }
  .item-lists .item-list h4 {
    font-size: 15px;
    font-weight: 300;
    line-height: 1.75em;
    margin: 0 0 15px 0;
  }

  @media screen and (max-width:768px) {
    body {
      padding: 0;
    }
    h1 {
      font-size: 25px;
      padding: 0 15px 15px;
      right: 0px;
      transform: rotate(0deg);
    }
    h2 {
      padding: 0 0 0 30px;
    }
    .container {
      width:100%;
      margin: 50px auto 0;
    }
  }
</style>

ルーティングと組み合わせたSSR

上記で作ったindex.vueから
各ユーザーの投稿した記事の一覧を作りたいと思います。

pages/index.vue
- <p>by {{ item.user.id }}</p>
+ <p>by <nuxt-link :to="`/users/${ item.user.id }`">{{ item.user.id }}</nuxt-link></p>

<nuxt-link :to="/users/${ item.user.id }">
※nuxt-linkタグはNuxtのルーティングです(最終aタグになります)。
※${ item.user.id }はQiitaのユーザーIDになります。
受け渡し先では$route.params.idで受け取れます

各ユーザー投稿記事一覧を作る

pages/users/_id.vueを作成
_id.vueファイルを作ることで$route.params.idで受け取れるデータになります

pages/users/_id.vue
<template>
  <section class="container">
    <nuxt-link class="navi" :to="`/`">&gt;Top</nuxt-link>
    <h2>{{ user.id }}さん 投稿一覧</h2>

    <ul class="item-lists">
      <li class="item-list" v-for="item in items" :key="item.id">
        <h3>
          <span><a :href="item.url" target="_blank">{{ item.title }}</a></span>
          <p>
            by <nuxt-link :to="`/users/${ item.user.id }`">{{ item.user.id }}</nuxt-link>
          </p>
        </h3>
        <h4>{{ item.body.slice(0, 100) }}.....</h4>
      </li>
    </ul>
    <p class="article-write-user">記事を書いた人</p>
    <aside class="user">
        <img :src="user.profile_image_url" alt="Profile Image">
        <div class="user-detail">
            <div class="user-name">{{ user.name }}</div>
            <div class="user-id">@{{ user.id }}</div>
            <p class="user-description">{{ user.description || 'No description' }}</p>
        </div>
    </aside>
  </section>
</template>

<script>
export default {
  async asyncData({ route, app }) {
    const user = await app.$axios.$get(`https://qiita.com/api/v2/users/${route.params.id}`);
    const items = await app.$axios.$get(`https://qiita.com/api/v2/items?query=user:${route.params.id}`);
    // console.log(user);
    // console.log(items);
    return {
      user,
      items,
    }
  }
}
</script>

<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    body {
        border-top: 5px solid rgb(0, 199, 0);
        padding: 0 0 100px 0;
        background: #efefef;
    }
    a {
        color: crimson;
    }
    ul li {
        list-style: none;
    }
    h1 {
        text-align: center;
        font-size: 35px;
        line-height: 1.25em;
        margin: 0 0 50px 0;
        padding: 0 0 5px 0;
        transform: rotate(-5deg);
        color: rgb(0, 199, 0);
        border-bottom: 2px solid rgb(0, 199, 0);
        right: 70px;
        position: relative;
    }
    h2 {
        font-size: 20px;
        font-weight: 500;
    }
    .container {
        width: 60%;
        margin: 50px auto;
    }
    .container .navi {
        font-size: 18px;
        text-decoration: none;
        color: rgb(0, 199, 0);
        top: 10px;
        left: 10px;
        position: fixed;
    }
    .item-lists {
        margin: 15px 0 0 0;
    }
    .item-lists .item-list {
        padding: 25px 30px 15px;
        border-bottom: 1px solid #ccc;
        background: #fff;
    }
    .item-lists .item-list:last-child {
        border-bottom: none;
    }
    .item-lists .item-list h3 {
        font-size: 18px;
        font-weight: 500;
        line-height: 1.5em;
        letter-spacing: -0.25px;
        margin: 0 0 10px 0;
    }
    .item-lists .item-list h3 p {
        font-size: 12px;
        font-weight: 100;
    }
    .item-lists .item-list h3 p a {
        font-size: 15px;
        color: rgb(0, 82, 0);
        text-decoration: none;
    }
    .item-lists .item-list h4 {
        font-size: 15px;
        font-weight: 300;
        line-height: 1.75em;
        margin: 0 0 15px 0;
    }
    .article-write-user {
        font-size: 15px;
        font-weight: bold;
        letter-spacing: -0.25px;
        margin: 70px 0 0 0;
    }
    .user {
        display: flex;
        width: 100%;
        margin: 10px auto 0;
        padding: 30px;
        background: #fff;
    }
    .user img {
        width: 125px;
        height: 125px;
        object-fit: cover;
        margin: 0 20px 0 0;
    }
    .user .user-detail .user-name {
        font-size: 25px;
        font-weight: bold;
    }
    .user .user-detail .user-id {
        font-size: 12px;
        font-weight: 100;
        margin: 0 0 20px 0;
    }
    .user .user-detail .user-description {
        text-align: justify;
        font-size: 14px;
        line-height: 1.5em;
    }

    @media screen and (max-width:768px) {
        body {
            padding: 0;
        }
        h1 {
            font-size: 25px;
            padding: 0 15px 15px;
            right: 0px;
            transform: rotate(0deg);
        }
        h2 {
            padding: 0 0 0 30px;
        }
        .container {
            width:100%;
            margin: 50px auto 0;
        }
        .article-write-user {
            padding: 0 0 0 30px;
        }
        .user {
            display: block;
            text-align: center;
            width: 85%;
            margin: 10px auto 30px;
            padding: 20px;
        }
        .user img {
            border-radius: 50%;
        }
    }
</style>
// QiitaのAPIから各ユーザー情報をuser変数に格納
const user = await app.$axios.$get(`https://qiita.com/api/v2/users/${route.params.id}`);

// そのuserのアイテム一覧をitems配列に格納
const items = await app.$axios.$get(`https://qiita.com/api/v2/items?query=user:${route.params.id}`);

head情報を書き換える

共通タイトルの設定

nuxt.config.js
head: {
    title: process.env.npm_package_name || '',
    // 追加
    titleTemplate: 'Nuxt viewer | %s',
    // .....省略
},

Nuxt viewer - 各ページ共通で入る値
%s - titleプロパティの値が入る

ページごとにの設定

%sはnuxt.config.jsの中のtitleプロパティが入るが
各ページでtitleの値を変えてあげれば設定変更が可能です。

pages/users/_id.vue
export default {
    // 追加
    head() {
        return {
            // QiitaのユーザーIDにする
            title: this.user.id
        }
    },
    // .....省略
}

Vuexストアを導入する

NuxtのVuexはクラシックモードとモジュールモードの2種類

クラシックモード - 最もシンプルなVuexストア ※今回はこちら
モジュールモード - 本格的なアプリケーション開発に向いたVuexストア

Vuexの雛形用意

store/index.js
import Vuex from 'vuex';

const store = {
state: {
        items: [],
    },
}

export default () => new Vuex.Store(store);

実際に書いていきます。

store/index.js
import Vuex from 'vuex';

const store = {
    state: {
        items: [],
        users: {},
        userItems: {},
    },
    getters: {
        items: (state) => state.items,
        users: (state) => state.users,
        userItems: (state) => state.userItems
    },
    mutations: {
        setItems (state, { items }) {
            state.items = items;
        },
        setUser (state, { user }) {
            state.users[user.id] = user;
        },
        setUserItems(state, { user, items }) {
            state.userItems[user.id] = items;
        }
    },
    actions: {
        async fetchItems({ commit }) {
            const items = await this.$axios.$get(
                'https://qiita.com/api/v2/items?query=tag:nuxt.js'
            );
            commit('setItems', { items });
        },
        async fetchUserInfo({ commit }, { id }) {
            const [user, items] = await Promise.all([
                this.$axios.$get(`https://qiita.com/api/v2/users/${id}`),
                this.$axios.$get(`https://qiita.com/api/v2/items?query=user:${id}`)
            ]);
            commit('setUser', { user });
            commit('setUserItems', { user, items });
        }
    }
};

export default () => new Vuex.Store(store);

続いてpages/index.vueとpages/user/_id.vueを改修
※templateとstyleは変更なし

pages/index.vue

<script>
import { mapGetters } from 'vuex';

export default {
  // Vuex使用なし
  // async asyncData({ app }) {
  //   const items = await app.$axios.$get('https://qiita.com/api/v2/items?query=tag:nuxt.js');
  //   return {
  //     items
  //   }
  // }

  // Vuex
  async asyncData({ store }) {
    if (store.getters['items'].length) { // length = indexに+1したものを返す
      return
    }
    await store.dispatch('fetchItems');
  },
  computed: {
    ...mapGetters(['items'])
  }
}
</script>
pages/user/_id.vue
<script>
import { mapGetters } from 'vuex';

export default {
    head() {
        return {
            title: this.user.id
        }
    },

    // Vuex使用なし
    // async asyncData({ route, app }) {
    //     const user = await app.$axios.$get(`https://qiita.com/api/v2/users/${route.params.id}`);
    //     const items = await app.$axios.$get(`https://qiita.com/api/v2/items?query=user:${route.params.id}`);
    //     return {
    //         user,
    //         items,
    //     }
    // }

    // Vuex
    async asyncData({ route, store, redirect }) {
        if(store.getters['users'][route.params.id]) {
            return
        }
        try {
            await store.dispatch('fetchUserInfo', { id: route.params.id })
        } catch (e) {
            redirect('/')
        }
    },
    computed: {
        user() {
            return this.users[this.$route.params.id]
        },
        items() {
            return this.userItems[this.$route.params.id] || []
        },
        ...mapGetters(['users', 'userItems'])
    }
}
</script>
4
6
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
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?