自前のブログ作り⑧ ー Nuxt.jsとApollo ClientでStrapiのGraphQLを表示する

schedule Published
published_with_changes Updated
Category folderMake
format_list_bulleted Contents

Nuxt.jsとApollo Clientで、GraphQLのデータを取得して表示します。
今回が初見ですが、本格的に学ぶのはNuxt3(Vue3対応)になってからでいいやと思い、Nuxt2系で適当に良い感じで進められました。プロトタイプで作ったCSSとJavaScriptはVue.jsで書き直さずにそのまま外部ファイルで使用しました。

Nuxt.jsをインストール

まずはローカル環境でやってみました。
プロジェクトを作成するディレクトリに移動します。(Node.jsはStrapi導入時にインストール済みです)
公式サイトopen_in_newに載っているコマンドを実行してプロジェクトを作成します。

$ npx create-nuxt-app fugafuga #fugafugaは任意のプロジェクト名

コマンドを実行すると対話形式で条件を訪ねてくるので、入力/選択していきます。 分からないものは未選択で進めました。

create-nuxt-app v3.7.1
✨  Generating Nuxt.js project in fugafuga
? Project name: (fugafuga)
? Programming language: (Use arrow keys)
> JavaScript 
  TypeScript 
? Package manager: 
  Yarn
> Npm
? UI framework: (Use arrow keys)
> None
  Ant Design Vue
  BalmUI
  Bootstrap Vue
  Buefy
  Chakra UI
  Element
  Framevuerk
  Oruga
  Tachyons
  Tailwind CSS
  Windi CSS
  Vant
  View UI
  Vuetify.js
? Nuxt.js modules: (Press  to select, <a> to toggle all, <i> to invert selection)
>( ) Axios - Promise based HTTP client
 ( ) Progressive Web App (PWA)
 ( ) Content - Git-based headless CMS
? Linting tools: (Press  to select, <a> to toggle all, <i> to invert selection)  
>( ) ESLint
 ( ) Prettier
 ( ) Lint staged files
 ( ) StyleLint
 ( ) Commitlint
? Testing framework: (Use arrow keys)
> None
  Jest
  AVA
  WebdriverIO
  Nightwatch
? Rendering mode: (Use arrow keys)
> Universal (SSR / SSG)
  Single Page App
? Deployment target: (Use arrow keys)  
> Server (Node.js hosting)
  Static (Static/Jamstack hosting)  
? Development tools: (Press  to select, <a> to toggle all, <i> to invert selection)
>( ) jsconfig.json (Recommended for VS Code if you're not using typescript)
 ( ) Semantic Pull Requests
 ( ) Dependabot (For auto-updating dependencies, GitHub only)
? What is your GitHub username? (github_username)
? Version control system: (Use arrow keys)  
> Git
  None

全て答えるとインストールされます。

Installing packages with npm
︙

🎉  Successfully created project fugafuga

  To get started:

        cd fugafuga
        npm run dev

  To build & start for production:

        cd fugafuga
        npm run build
        npm run start

インストール完了後に教えてもらったnpmコマンドで起動してみます。

$ cd fugafuga #プロジェクトディレクトリに移動
$ npm run dev #開発モードで起動
︙
︙
✓ Client
Compiled successfully in 24.72s

✓ Server
Compiled successfully in 22.29s

i Waiting for file changes
i Memory usage: 156 MB (RSS: 188 MB) 
i Listening on: http://localhost:3000/ 

http://localhost:3000/で確認できました。

remove [ components/Tutorial.vue ]
coding in [ pages/index.vue ]

とりあえずデフォルトの画面でヒントを貰ったので、頭に入れておきます。

Nuxt.jsの「apollo module 」をインストール

とりあえずGraphQLを扱うのに便利らしいので、先にインストールしておきます。
一旦先ほどのNuxtを停止して、Nuxt CommunityのGitHubopen_in_newにあるコマンドを実行します。

作成したNuxtプロジェクトディレクトリで実行
$ npm install --save @nuxtjs/apollo

インストールが完了したら、構築の準備完了です。

HTMLプロトタイプをNuxt.jsで表示する

API云々以前に、普通に自分のページをSSRで描画させるのすら分かりません。
まずは予め作っておいたHTMLプロトタイプを、Nuxtで見れるようにしたいと思います。
公式サイトTOPopen_in_newの特徴紹介を見ると色々あるのですが、まずは下記3つの情報を漁ればできそうです。

  • File-system Routing
    Automatic routing and code-splitting for every page.
  • Strong Conventions
    Efficient teamwork with a strong directory structure and conventions.
  • Components Auto-import
    Use your components, Nuxt will import them with smart code-splitting.

作成したプロジェクト内のディレクトリやブラウザの表示なども見てみます。
Chrome拡張機能の「Vue.js devtoolsopen_in_new」を使うと把握しやすくなります。

ディレクトリ構成

作成したNuxtプロジェクト「fugafuga」の中身(細かいファイルなどは省略)
foldernode_modules  #中は省略。apollo-moduleもココに追加された。
└ ︙
folder.nuxt  #中は省略。
└ ︙
folderlayouts*  #上記.nuxtディレクトリ内から持ってきた。descriptiondefault.vue  #共通テンプレート
folderpages  #ページテンプレート:この中のディレクトリやファイルを自動でルーティング設定してくれるdescriptionindex.vue  #Topページ
foldercomponents  #ヘッダーとかフッターとか部品descriptionNuxtLogo.vue
└ descriptionTutorial.vue
folderstatic  #静的ファイルを置く、ドメインルート直下でアクセスできるdescriptionfavicon.ico
folderstore   #とりあえず今は知らない
descriptionnuxt.config.js  #共通設定ファイル

従来のCMS(WordPressとかMovableTypeとか)のテンプレートの感じでいけそうです。
/layouts/default.vueはやり方に自信がないですが、いじってbuildすると元に戻されたので、外に出してみたら希望通りに動きました。

なんとなく掴めたので、プロトタイプのHTMLを分割して各vueファイルにしていきます。

NuxtとVueの基礎知識(v2)

Vue.jsの知識もゼロなので、分割作業で学んだところをメモしておきます。(将来更新するときの備え)
NuxtとVueのどっちの知識なのかは区別できてないかもしれません。

  • Vueファイル

    1つのファイルに、HTMLとCSSとJavaScriptが書ける。
    <template>内は1つのDOMツリーしか置けない。
    今回はプロトタイプのCSSとScriptをVueファイルに合わせて分割するのが面倒なのでHTMLだけにしました。

    <template>
    ︙ HTML 直下に兄弟要素を書けない。divとかでラップして1つのDOMツリーなら良い
    </template>
    
    <style>
    ︙ CSS
    </style>
    
    <script>
    ︙ JS, Vue
    </script>
    
  • <head>~</head>内のtitleやmeta、外部ファイルの読み込みなど

    全体で共通ならnuxt.config.jsに、特定のページだけなら該当ページのvueファイルの<script>~</script>内に書けば良い。
    「body: true」を付けると</body>の直前になる

    export default {
      head: {
        title: 'ベアマケR',
        meta: [
          { charset: 'utf-8' },
          { name: 'viewport', content: 'width=device-width, initial-scale=1' },
          { hid: 'description', name: 'description', content: '' },
          { name: 'format-detection', content: 'telephone=no' }
        ],
        script: [
          { src: '/assets/js/hogehoge.js', defer: true },
          { src: '/assets/js/console.log.js', body: true, defer: true } // </body>直前になる
        ],
        link: [
          { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' },
          { rel: 'stylesheet', href: '/assets/css/common.css', async: true }
        ]
      }
    }
    
  • <html>や<body>に属性を付けたい

    全体で共通ならnuxt.config.jsに、特定のページだけなら該当ページのvueファイルに書けば良い。

    export default {
      head: {
        htmlAttrs: {
          lang: 'ja'
        },
        bodyAttrs: {
          id: 'hogehoge'
        }
      }
    }
    
  • コンポーネントを読み込んで表示する

    仮にheader.vueとfooter.vueのコンポーネントがあった場合です。

     /layouts/default.vue(全体共通テンプレート)
    <template>
    <div>
      <header /> headerコンポーネント表示
      <Nuxt /> Nuxt仕様:ルーティングに応じたページのvueファイルを表示する
      <footer /> footerコンポーネント表示
    </div>
    </template>
    
    <script>
    import header from '@/components/header.vue'
    import footer from '@/components/footer.vue'
    export default {
      components: {
        header,
        footer
      }
    };
    </script>
    

GraphQLのデータを取得して表示する

今はコンテンツがテキストベタ打ちなので、StrapiのGraphQLで取得したデータを表示できるようにします。 この辺りの作業は、Strapiのブログの下記リンクの記事などが分かりやすいです。

Strapiブログ:Build a blog with Nuxt (Vue.js), Strapi and Apolloopen_in_new

apollo moduleの設定

nuxt.config.jsにモジュールの使用とエンドポイントを設定します。

nuxt.config.js
export default {
︙ 
  modules: [  
    '@nuxtjs/apollo'
  ],
  apollo: {  
    clientConfigs: {
      default: {
        httpEndpoint: 'https://example.com/strapi/graphql'
      }
    }
  },
︙ 
}

apollo moduleでGraphQLを取得する

GraphQLのクエリファイルを作成して、コンポーネントでデータを取得します。
下記は記事一覧ページに、記事ID/カテゴリー名/記事タイトル/アイキャッチ画像を表示する例です。
(仮定のクエリですので、実際はスキーマに合わせたクエリを書きます。)

/queries/list.gql(クエリファイルを作成)
query List {
  articles {
    id
    category
    title
    img {
      url
    }
  }
}
/components/list.vue(記事一覧のコンポーネント)
<template>
<div>
  <!-- v-forで記事データ分を繰り返し -->
  <article v-for="article in reverseArticles" :key="article.id">
    <span>article.category</span>
    <h1><a v-bind:href="'/article/' + article.id">article.title</a></h1>
    <!-- v-ifでデータが存在するときのみ表示(elseでデフォルト画像を表示でも) -->
    <p v-if="article.img" ><img v-bind:src="'/strapi/' + article.img.url"></p>
  </article>
</div>
</template>

<script>
import listQuery from '~/queries/list.gql' //クエリファイルをインポート
export default {
  data() {
    return {
      articles: []  // クエリで取得のデータ定義
    }
  },
  apollo: {
    articles: {
      prefetch: true,
      query: listQuery // インポートしたクエリを指定
    }
  },
  computed: {
    reverseArticles() {
      return this.articles.slice().reverse() //並び順を反転
    }
  } 
}
</script>

次は記事詳細での例です。
実際にはあり得ないデータ項目ですが、記事一覧と同様に、記事ID/カテゴリー名/記事タイトル/アイキャッチ画像を表示してみます。

/queries/article.gql(クエリファイルを作成)
query Article($id: ID!) { 
  article(id: $id) {
    id
    category
    title
    img {
      url
    }
  }
}
/pages/article/_id.vue(記事詳細のページコンポーネント)
<template>
<article>
    <span>article.category</span>
    <h1>article.title</h1>
    <p v-if="article.img"><img v-bind:src="'/strapi/' + article.img.url"></p>
</article>
</template>

<script>
import articleQuery from '~/queries/article.gql' //クエリファイルをインポート
export default {
  data() {
    return {
      article: {}  // クエリで取得のデータ定義
    }
  },
  apollo: {
    article: {
      prefetch: true,
      query: articleQuery, // インポートしたクエリを指定
      variables () {
        return { id: parseInt(this.$route.params.id) } //クエリのID変数にURLパラメータのIDを指定
      }
    }
  }
}
</script>

こんな感じで触りながら作りました。

NuxtとVueの基礎知識(v2)

データを反映する作業で学んだところをメモしておきます。

  • 動的ルーティング

    NuxtのPages内の自動ルーティングで、ディレクトリ名やファイル名にアンダースコアを付けると動的なURLが生成される。/article/1, /article/2, /article/3 …など→/pages/article/_id.vueのようにファイル名に_を付ける。

  • 親から子へのコンポーネント間のデータ受け渡し

    親側で子コンポーネントにv-bindで送り、子側はpropsで受け取る。
    HOMEのページコンポーネントで受け取ったURLのパラメーターを、記事一覧の子コンポーネントへ渡して一覧の絞り込みに使用しました。

  • asyncDataメソッド

    ページコンポーネントでのみ使用可能。
    ページの表示前にコンポーネントのデータをネットワークから取得できる、非同期データ機能。上記と同じく、HOMEでの記事一覧表示に使用しました。

  • v-model(双方向データバインディング)

    グローバルメニューでカテゴリやタグキーワードでの絞り込み選択時、記事数を表示するため使用しました。

おわりに

“YAGNI”で必要そうなものだけ情報収集しました。作るものが決まっておらず、単にNuxtとVueを学習するとなると壮大で大変そうですね。

公開してからすぐにNuxt3(Vue3対応)が出てしまいました。
ベータ版からアルファ版になったら、アップデートしたいと思います。
Strapiもv4のベータ版が出てます。大変じゃありませんように…

次はいよいよ公開作業です。