Photoruction工事中!

Photoructionの開発ブログです!

Nuxt3 × Vuetify × Instagram APIで写真検索アプリ作ってみよう

こんにちは!株式会社フォトラクションでプロダクトマネージャーをしています南風原はえばる)です。

Photoruction Advent Calendar 2022の12日目の記事になります。

はじめに

ついに、待望のNuxt3が正式リリースされましたね👏

これからNuxt3を使ってみようと思っている初心者向けに、Nuxt3 × Vuetify × Instagram APIを用いた簡単な写真検索アプリをハンズオン形式で紹介していきたいと思います。

[前提]開発環境の各種バージョンは以下になります

os: macOS Monterey
node: v18.12.1
yarn: 1.22.11

Nuxt3をインストールしよう

npx nuxi init nuxt-instagram

cd nuxt-instagram

yarn install

yarn dev -o

Vuetify3をインストールしよう

yarn add vuetify@next mdi
yarn add -D sass

Nuxt3でVuetifyを利用できるようにしよう

// ./plugins/vueitfy.ts
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';

export default defineNuxtPlugin((nuxtApp) => {
  const vuetify = createVuetify({
    components,
    directives,
  });

  nuxtApp.vueApp.use(vuetify);
});
// ./nuxt.config.ts
export default defineNuxtConfig({
    css: ['vuetify/lib/styles/main.sass', 'mdi/css/materialdesignicons.min.css'],
    build: {
      transpile: ['vuetify'],
    },
})

Instagram Graph APIを利用できる準備をしよう

Instagramプロアカウントを作成

  1. Instagramアカウントを作成する
  2. 設定から「プロアカウントに切り替える」をクリックしプロアカウントに切り替える

Facebookページを作成

  1. Facebookにログインし、任意のFacebookページを作成する

    ※ページ名に「Instagram」など固有サービス名を含めるとページがBANされてAPI利用できないので注意!

  2. FacebookページとInstagramプロアカウントの紐づけをする

    1. 右上のアイコンクリック > 設定とプライバシー > 設定 > リンク済みのアカウント

    2. 「アカウントをリンク」よりFacebookページとInstagramアカウントを紐付けます

Facebookアプリを作成

  1. Facebook for Developersにアクセスし「利用を開始」するよりFacebook for Developersアカウントを作成する
  2. 「アプリを作成」> 「ビジネス」> 基本情報を入力する
  3. FacebookアプリにInstagram APIを追加する
    1. InstagramグラフAPI」を追加する
    2. 左サイドメニュー「設定」の中の「ベーシック」をクリック
    3. アプリIDとApp Secretキーをメモし、「変更を保存」をクリック

アクセストークンを取得

  1. 1番目のアクセストークンを取得する

    1. ヘッダーの「ツール」から「InstagramグラフAPIエクスプローラー」をクリック
    2. 作成したアプリを選択し、「ページアクセストークンを取得」をクリックし、作成したFacebookページを選択し「完了」をクリック
    3. ↓1番目のアクセストークンをコピーする

  2. 2番目のアクセストークンを取得する

// ブラウザで下記URLにアクセスする
https://graph.facebook.com/v15.0/oauth/access_token?grant_type=fb_exchange_token&client_id=${アプリID}&client_secret=${app secret}&fb_exchange_token=${1番目のトークン}
{
    "access_token": "2番目のトークン",
    "token_type": "bearer",
    "expires_in": 5183464
}
  1. 3番目のアクセストークンを取得する
// ブラウザで下記URLにアクセスする
https://graph.facebook.com/v15.0/me?access_token=${2番目のトークン}
{
    "name": "山田 太郎",
    "id": "12345678"
}

上で取得したIDを使って、ブラウザで下記URLにアクセスします。

https://graph.facebook.com/v15.0/${id}/accounts?access_token=${2番目のトークン}
// 3番目のアクセストークンが発行される
{
    "data": [
        {
            "access_token": "3番目のトークン",
            "category": "xxxx",
            "category_list": [
                {
                    "id": "123456",
                    "name": "xxxx"
                }
            ],
            "name": "Facebookページ名",
            "id": "FacebookページID"
        }
    ],
    "paging": {
        "cursors": {
            "before": "xxxx",
            "after": "xxxx"
        }
    }
}

InstagramビジネスアカウントIDの取得

// 上で取得したIDを使って、ブラウザで下記URLにアクセスする
https://graph.facebook.com/v15.0/${FacebookページID}?fields=id,name,instagram_business_account&access_token=${3番目のトークン}
// InstagramビジネスアカウントID取得結果
{
    "id": "FacebookページID",
    "name": "Facebookページ名",
    "instagram_business_account": {
        "id": "InstagramビジネスアカウントID"
    }
}

「3番目のアクセストークン」と「InstagramビジネスアカウントID」の取得ができたので、晴れてInstagram GraphAPIの利用が可能となります!

.envに環境変数を追加しよう

.envに環境変数を登録後、nuxtから呼び出せるようにnuxt.config.tsにも定義します。

// ./nuxt.config.ts
export default defineNuxtConfig({
    runtimeConfig: {
      public: {
        instagramApiUrl: process.env.INSTAGRAM_API_URL,
        userId: process.env.USER_ID,
        accessToken: process.env.ACCESS_TOKEN,
      }
    }
})

検索フォームを作成してみよう

// ./app.vue
<template>
  <SearchInput />
</template>
// ./components/SearchInput.vue
<script setup lang="ts">
interface State {
    loaded: boolean
    loading: boolean
    value: string
}

const state = reactive<State>({
    loaded: false,
    loading: false,
    value: ''
})

interface Emits {
  (e: 'search', value: string): void
}
const emit = defineEmits<Emits>()

const handleSearch = () => {
    state.loading = true
    setTimeout(() => {
        state.loading = false
        state.loaded = true
    }, 2000)
    emit('search', state.value)
}
</script>
<template>
    <v-text-field
        v-model="state.value"
        :loading="state.loading"
        density="compact"
        variant="solo"
        label="検索"
        append-inner-icon="mdi-magnify"
        single-line
        hide-details
        @click:append-inner="handleSearch"
     />
</template>

↓こういう感じで、表示されればOK👌

ハッシュタグ検索機能をつくろう

APIエンドポイントを定義

外部APIを直接呼び出そうとするとCORSエラーになってしまうので、server/apiAPIエンドポイントを定義して呼び出すようにします。

ハッシュタグのIDを取得

// server/api/hashtagId.ts
export default defineEventHandler ( async (event) => {
    const config = useRuntimeConfig()
    const query = getQuery(event)
    const result = await $fetch(`${config.public.instagramApiUrl}/ig_hashtag_search`, {
        params: query
    })
    return result
})

ハッシュタグの付いたメディアを取得

// server/api/recentMedia/[hashtagId].ts
export default defineEventHandler ( async (event) => {
    const config = useRuntimeConfig()
    const query = getQuery(event)
    const result = await $fetch(`${config.public.instagramApiUrl}/${event.context.params.hashtagId}/recent_media`, {
        params: query
    })
    return result
})

データ取得に関するロジックを書く

composablesディレクトリにデータ取得などのロジックを書くことで、Vue側で自動インポートされて利用できるようになります。

// ./composables/useInstagram.ts
import { Media } from '@/@types/instagram'

export const useInstagram = () => {
    const config = useRuntimeConfig()
    const medias = ref<Media[]>([]);

    const getHashtagId = async (hashtag: string) => {
        const response = await $fetch('/api/hashtagId', {
            params: {
                user_id: config.public.userId,
                access_token: config.public.accessToken,
                q: hashtag
            }
        })
        return response.data[0].id
    }

    const getRecentMedia = async (hashtag: string) => {
        const hashtagId = await getHashtagId(hashtag)
        const response = await $fetch(`/api/recentMedia/${hashtagId}`, {
            params: {
                user_id: config.public.userId,
                access_token: config.public.accessToken,
                fields: 'id,media_url,permalink,media_type,timestamp'
            }
        })
        medias.value = response.data.filter(i => {
            if(i.media_url && i.media_type === 'IMAGE') {
                return true
            }
            return false
        })

        return medias.value
    } 

    return {
        getHashtagId,
        getRecentMedia,
        medias
    }
}

Vue側でComposablesな関数を呼び出してみよう

<script setup lang="ts">
import { useInstagram } from '@/composables/useInstagram'
const { getRecentMedia, medias } = useInstagram()

const handleSearch = async (keyword: string) => {
  await getRecentMedia(keyword)
}
</script>

<template>
  <div class="wrapper">
    <SearchInput class="mt-8" @search="handleSearch" />
    <v-row class="my-8">
        <v-col
          v-for="media in medias"
          :key="media.id"
          class="d-flex child-flex p-2"
          cols="4"
        >
        <v-card min-width="100%">
          <v-img
            :src="media.media_url"
            aspect-ratio="1"
            cover
            class="align-end"
          >
            <template v-slot:placeholder>
              <v-row
                class="fill-height ma-0"
                align="center"
                justify="center"
              >
                <v-progress-circular
                  indeterminate
                  color="grey-lighten-5"
                ></v-progress-circular>
              </v-row>
            </template>
          </v-img>
        </v-card>
        </v-col>
    </v-row>
  </div>
</template>

<stype lang="scss" scoped>
  .wrapper {
    width: 65%;
    margin: 0 auto;
  }
</stype>

完成形

まとめ

実際作ってみて、Nuxt3を使う以前にInstagram Graph APIの事前準備のところで手こずってしまい意外と時間かかってしまいました。

久々に個人開発をして楽しかったので良しとします笑

Nuxt3になってからTypeScriptのサポートが強化されたのはもちろんのこと、ビルドのスピードがより速くなったり、各ディレクトリのルールに沿ってファイルを作成することで自動インポートをしてくれたり、書き方がシンプルになったり、などなどNuxt2よりも爆速で開発ができるようになった印象です。

まだまだ、情報が少ない部分もありますがぜひNuxt3を使っていきましょう〜♪

最後に、今回作成したものはGitHubにあげています。

少しでも参考になれば幸いです。

株式会社フォトラクションでは一緒に働く仲間を募集しています