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

schedule Published
published_with_changes Updated
Category folderMake
format_list_bulleted Contents

今回はNuxt.jsとApollo Clientをつかって、GraphQLのデータを取得してWebページに表示する。
Nuxt.jsは初めてさわるが、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を扱うのに便利らしいので、先に「apollo module 」をインストールしておく。
一旦先ほど起動したNuxtを停止して、Nuxt CommunityのGitHubopen_in_newにあるコマンドを実行する。

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

インストールしたら準備完了。次は構築に進む。

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

API云々以前に、普通に自分で書いたHTMLを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のパラメーターを、記事一覧の子コンポーネントへ渡して一覧の絞り込み表示に使用した。(例:/?cats=1のパラメーターを使ってMAKEカテゴリで絞り込んだ記事一覧表示)これがいい方法なのかは分からないが。。

  • asyncDataメソッド

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

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

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

おわりに

“YAGNI”で必要そうなものだけを情報収集した。単にNuxtとVueを学習するとなると壮大で大変そうだが、ただ表示できればいいだけなので思ったほど時間はかからずに済んでよかった。

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

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