VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう
  • 2022.01.12に公開
  • 2022.01.16に更新
  • ブログ構築TS
  • 6. Vuex×TypeScriptセットアップ
  • No.4 / 4

今回達成すること

前回作成したnuxtServerInitから、Contentful APIへリクエストを行います。

返されたContentfulコンテンツは、store/index.tsstate に保存し、最終的にindex.vueへ表示します。

2022-01-11 20-58-18

Vuex Postストアを作成する

前回store/index.tsに用意した、Actionメソッドのpost/getEntriesを用意しましょう。

store/index.ts(前回のコード)
async getContentfulApi ({ dispatch }) {
  // これの呼び出し元を追加する
  await dispatch('post/getEntries')
}

storeディレクトリ内にpost/index.tsを作成してください。

% mkdir store/post && touch $_/index.ts

post/index.tsを仮編集します。

mutationsは使用しないので宣言しません。

store/post/index.ts
import { getterTree, actionTree } from 'typed-vuex'
import { BlogCategory, BlogPost, BlogTag } from '../types'

export const state = () => ({})

export const getters = getterTree(state, {})

export const actions = actionTree({ state, getters }, {})

Contentful APIで使用する固定値を用意する

stateにContentful APIで使用する値を用意します。

store/post/index.ts
export const state = () => ({
  content: {
    type: {
      category: 'blogCategory' as string,
      post: 'blogPost' as string,
      tag: 'blogTag' as string
    },
    limit: 200 as number,
    // 公開日 降順
    order: '-fields.publishDate' as string
  } as const
})

Vuex Stateの値を読み取り専用としたい時

stateの値を読み取り専用としたい場合はas constを使用します。

as constを宣言するとreadonlyが付与され、変更不可の読み取り専用の値となります。

今回は、contentオブジェクトにas constを宣言し、Contentful APIで使用する全ての値を読み取り専用としています。

2022-01-11 12-29-49

APIリクエストを行うActionメソッドを作成する

actionsにAPIリクエストメソッドを追加します。

plugins/contentful.tsをインポートし、レスポンスのインターフェースを用意します。

store/post/index.ts
import { getterTree, actionTree } from 'typed-vuex'
import { BlogCategory, BlogPost, BlogTag } from '../types'
// 追加
import client from '~/plugins/contentful'

type RelatedEntries = (BlogCategory | BlogTag)[]
interface Entry {
  includes: {
    Entry: RelatedEntries
  }
  items: BlogPost[]
}

actionsにContentful APIリクエスト処理を記述します。

これはpages/index.vueに記述したasyncDataと同じ処理を行なっています。

store/post/index.ts
export const actions = actionTree({ state, getters }, {
  async getEntries ({ state, getters, commit }) {
    await client.getEntries({
      content_type: state.content.type.post,
      limit: state.content.limit,
      order: state.content.order
    })
      .then((entry: Entry) => {
        const reletedEntry = getters.separateEntries(entry.includes.Entry)
        const categories = getters.sortCategories(reletedEntry.categories)
        const tags = getters.sortTags(reletedEntry.tags)

        commit('setCategories', categories, { root: true })
        commit('setTags', tags, { root: true })
        commit('setPosts', entry.items, { root: true })
      })
      .catch((error: object) =>
        console.log(error)
      )
  }
})
  • commit(, , { root: true }) ... index.tsmutationsを呼び出す場合は、{ root: true }を第三引数に追加する。{ root: true }がない場合はローカルのmutationsが呼ばれる。

データ整形するGetter関数を作成する

gettersにレスポンスデータの整形処理を追加します。

separateEntriesは、blogPostモデルに関連づいたblogCategoryとblogTagモデルを、それぞれの配列に振り分けます。

store/post/index.ts
export const getters = getterTree(state, {
  // blogPostモデルに関連づくコンテンツを配列に振り分ける
  separateEntries: (state: PostState) => (entries: RelatedEntries):
  { categories: BlogCategory[], tags: BlogTag[] } => {
    // push時点ではTypeエラーとなるのでany型で宣言し、Getterの戻り値で型指定を行う
    const categories: any[] = []
    const tags: any[] = []

    for (const entry of entries) {
      const currentContentType: string = entry.sys.contentType.sys.id

      if (currentContentType === state.content.type.category) {
        categories.push(entry)
      } else if (currentContentType === state.content.type.tag) {
        tags.push(entry)
      }
    }

    return { categories, tags }
  }
})

Getter関数の戻り値を明示的に指定する理由

変数の宣言時点でBlogCategory[]の型を宣言するとpush(entry)ができません。

// NG
const categories: BlogCategory[] = []

for (const entry of entries) {
  // TypeError
  categories.push(entry)
}

これはfor文のentryが、BlogCategory | BlogTag 型であるためです。

そこで変数はany型で宣言し、Getter関数の戻り値に対して型を指定しています。

separateEntries: (state: PostState) => (entries: RelatedEntries):
  { categories: BlogCategory[], tags: BlogTag[] } => {
    // OK
    const categories: any[] = []

    for (const entry of entries) {
      categories.push(entry)
    }

    return { categories, tags }
  }

これにより、separateEntriesの型を保証しています。

2022-01-11 17-54-57

Sort関数をGetterに作成する

gettersに並び替えを行う関数を2つ追加します。

  1. sortCategories ... blogCategoryモデルをsort 昇順に並び替える関数
  2. sortTags ... blogTagモデルをcreatedAt(作成日) 降順に並び替える関数
store/post/index.ts
export const getters = getterTree(state, {
  ...
  // カテゴリーの並び替え(sort: 昇順)
  sortCategories: () => (categories: BlogCategory[]) =>
    categories.sort((a: BlogCategory, b: BlogCategory) =>
      a.fields.sort - b.fields.sort
    ),

  // タグの並び替え(作成日: 降順)
  sortTags: () => (tags: BlogTag[]) =>
    tags.sort((a: BlogTag, b: BlogTag) => {
      switch (true) {
        case a.sys.createdAt < b.sys.createdAt: return 1
        case a.sys.createdAt > b.sys.createdAt: return -1
        default: return 0
      }
    })
})

stateに保存する前に並び替えを行うことで、Vueファイル上でデータが扱いやすくなります。

またこの処理は、% yarn generateコマンド時に実行されるため、クライアントの処理が減り、サイトの表示スピードが上がります。

このように、FullStaticモードのNuxtでは、決まりきった処理をできるだけサーバーサイドで行うことをおすすめします。

最終的なpost/index.ts

最終的なコードは以下の通りです。

store/post/index.ts
import { getterTree, actionTree } from 'typed-vuex'
import { BlogCategory, BlogPost, BlogTag } from '../types'
import client from '~/plugins/contentful'

type RelatedEntries = (BlogCategory | BlogTag)[]
interface Entry {
  includes: {
    Entry: RelatedEntries
  }
  items: BlogPost[]
}

export const state = () => ({
  content: {
    type: {
      category: 'blogCategory' as string,
      post: 'blogPost' as string,
      tag: 'blogTag' as string
    },
    limit: 200 as number,
    // 公開日 降順
    order: '-fields.publishDate' as string
  } as const
})

type PostState = ReturnType<typeof state>

export const getters = getterTree(state, {
  // blogPostモデルに関連づくコンテンツを配列に振り分ける
  separateEntries: (state: PostState) => (entries: RelatedEntries):
  { categories: BlogCategory[], tags: BlogTag[] } => {
    // push時点ではTypeエラーとなるのでany型で宣言し、Getterの戻り値で型指定を行う
    const categories: any[] = []
    const tags: any[] = []

    for (const entry of entries) {
      const currentContentType: string = entry.sys.contentType.sys.id

      if (currentContentType === state.content.type.category) {
        categories.push(entry)
      } else if (currentContentType === state.content.type.tag) {
        tags.push(entry)
      }
    }

    return { categories, tags }
  },

  // カテゴリーの並び替え(sort: 昇順)
  sortCategories: () => (categories: BlogCategory[]) =>
    categories.sort((a: BlogCategory, b: BlogCategory) =>
      a.fields.sort - b.fields.sort
    ),

  // タグの並び替え(作成日: 降順)
  sortTags: () => (tags: BlogTag[]) =>
    tags.sort((a: BlogTag, b: BlogTag) => {
      switch (true) {
        case a.sys.createdAt < b.sys.createdAt: return 1
        case a.sys.createdAt > b.sys.createdAt: return -1
        default: return 0
      }
    })
})

export const actions = actionTree({ state, getters }, {
  async getEntries ({ state, getters, commit }) {
    await client.getEntries({
      content_type: state.content.type.post,
      limit: state.content.limit,
      order: state.content.order
    })
      .then((entry: Entry) => {
        const reletedEntry = getters.separateEntries(entry.includes.Entry)
        const categories = getters.sortCategories(reletedEntry.categories)
        const tags = getters.sortTags(reletedEntry.tags)

        commit('setCategories', categories, { root: true })
        commit('setTags', tags, { root: true })
        commit('setPosts', entry.items, { root: true })
      })
      .catch((error: object) =>
        console.log(error)
      )
  }
})

これでVuexからサーバーサイドでContentful APIをリクエストできるようになりました。

Postストア型を$accessorに登録する

作成したPostストアの型を、$accessorに登録します。

store/index.tsへ移動してください。

store/index.ts
import { BlogCategory, BlogPost, BlogTag } from './types'

// 追加
// Import submodules
import * as post from '~/store/post'

export const accessorType = getAccessorType({
  ...
  modules: {
    // 追加
    post
  }
})

これでPostストアの型を登録できたので、$accessor.postを使用してもTypeエラーにはなりません。

VuexストアをVueファイルから呼び出そう

Vuexのコンテンツをpages/index.vueに表示します。

今あるコードを全て削除して、<script>タグをまるっと書き換えてください。

Vuexに保存したデータをasyncData()で取得するだけOKです。

pages/index.ts
<script lang="ts">
import { Context } from '@nuxt/types'
import { Component, Vue } from 'nuxt-property-decorator'
import { BlogCategory, BlogPost, BlogTag } from '~/store/types'

interface AsyncData {
  categories: BlogCategory[]
  posts: BlogPost[]
  tags: BlogTag[]
}

@Component
export default class IndexPage extends Vue {
  asyncData ({ app: { $accessor } }: Context): AsyncData {
    return {
      categories: $accessor.categories,
      posts: $accessor.posts,
      tags: $accessor.tags
    }
  }
}
</script>

HTMLにカテゴリー、タグ、ブログ記事を表示します。

カテゴリーは、Vuetifyのv-cardコンポーネントを使用してカード形式で表示します。

pages/index.vue(カテゴリー)
<template>
  <div>
    <v-container>
      <v-card-title
        class="font-weight-bold"
      >
        Category
      </v-card-title>

      <v-card-title
        class="font-weight-bold"
      >
        Category
      </v-card-title>

      <v-row>
        <!-- sm, md => Window幅がsm、md時にカラムを何分割するか(sm: 4/12, md: 3/12) -->
        <v-col
          v-for="(category, i) in categories"
          :key="`category-${i}`"
          cols="12"
          sm="4"
          md="3"
        >
          <!-- to => v-card全体をリンクに設定する -->
          <v-card
            max-width="300"
            class="mx-auto"
            :to="`/categories/${category.fields.slug}`"
          >
            <!-- v-if => imageプロパティが存在する場合に表示 -->
            <v-img
              v-if="category.fields.image"
              :src="`https://${category.fields.image.fields.file.url}`"
            />

            <v-card-title>
              {{ category.fields.sort }}.
              {{ category.fields.title }}
            </v-card-title>

            <v-card-subtitle>
              {{ category.fields.description }}<br>
              slug: {{ category.fields.slug }}<br>
              isMain: {{ category.fields.isMain }}
            </v-card-subtitle>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

タグはv-chipを使って表示します。

pages/index.vue(タグ)
<v-card-title
  class="font-weight-bold"
>
  Tag
</v-card-title>

<v-card>
  <v-card-text>
    <v-chip
      v-for="(tag, i) in tags"
      :key="`tag-${i}`"
      outlined
      :to="`/tags/${tag.fields.slug}`"
    >
      <v-img
        v-if="tag.fields.image"
        :src="`https://${tag.fields.image.fields.file.url}`"
        width="18"
        height="18"
        class="mr-2"
      />
      {{ tag.fields.title }}
    </v-chip>
  </v-card-text>
</v-card>

ブログ記事はv-listを使って表示します。

pages/index.vue(ブログ記事)
<v-card-title
  class="font-weight-bold"
>
  Post
</v-card-title>
<v-list>
  <!-- $router.push() => クライアントでページ遷移を行う(<nuxt-link>と同じ挙動) -->
  <v-list-item
    v-for="(post, i) in posts"
    :key="`post-${i}`"
    @click="$router.push(`/posts/${post.fields.category.fields.slug}/${post.fields.slug}`)"
  >
    <v-list-item-avatar
      tile
      width="160"
      height="90"
    >
      <v-img
        :src="`https://${post.fields.image.fields.file.url}`"
      />
    </v-list-item-avatar>

    <v-list-item-content>
      <v-list-item-subtitle>
        <nuxt-link
          :to="`/categories/${post.fields.category.fields.slug}`"
        >
          {{ post.fields.category.fields.title }}
        </nuxt-link>
      </v-list-item-subtitle>

      <!-- tag => DOMのタグを<a>タグに変更(default: 'dev') -->
      <v-list-item-title
        tag="a"
      >
        {{ post.fields.title }}
      </v-list-item-title>

      <v-list-item-subtitle>
        公開日: {{ post.fields.publishDate }}
      </v-list-item-subtitle>

      <!-- v-if => tagsは必須ではないので、存在する場合にのみループする -->
      <v-list-item-action-text
        v-if="post.fields.tags"
      >
        <!-- v-for => v-forとv-ifは同じタグで宣言しない。エラーの元となる -->
        <nuxt-link
          v-for="(tag, tagI) in post.fields.tags"
          :key="`tag-${tagI}`"
          :to="`/tags/${tag.fields.slug}`"
          class="mr-2"
        >
          #{{ tag.fields.title }}
        </nuxt-link>
      </v-list-item-action-text>
    </v-list-item-content>
  </v-list-item>
</v-list>

完成したindex.vue

カテゴリー、

2022-01-11 20-58-18

タグ、ブログ記事、それぞれが表示できました。

2022-01-11 20-58-35

ここまでの変更をNetlifyにデプロイする

最後に本番環境にデプロイして終わりましょう。

% yarn netlify:deploy

エラーが出るのは、ルート先のVueファイルを作成していないからです。

今は無視してOKです。

Error generating route "/demo-blog-03": This page could not be found 

デプロイが成功すればここまでの変更をGitHubにPushします。

% git add -A
% git commit -m "Add vuex post/index.ts get contentful api"
% git checkout master
% git merge 20220109_setup_nuxt_typed_vuex 
% git push

このチャプターまとめ

このチャプターでは、下記4つの作業を行いました。

  1. TypeScript環境のVuexモジュールの検討
  2. nuxt-typed-vuexのインストール ~ セットアップ
  3. Vuexへ型定義ファイルの追加
  4. Contentful APIレスポンスをVuexストアに保存(今ここ)

次回は?

現状、コンテンツを表示するページが作成されていないため、リンクをクリックしてもエラーになります。

次回は、このエラーに対応し、コンテンツを表示するページを作成します。

あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる。そんな方に向けた単発・短期間メンターサービスを行っています。
独学プログラマのサービス
ブログ構築TSの投稿
1
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #01
Nuxt.jsをローカルPCに立ち上げよう
2
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #02
Nuxt.jsプロジェクトをGitHubにPushしよう
3
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #03
nuxt-property-decoratorのインストールとTypeScriptのセットアップ
1
  • Vuetifyセットアップ
  • /
  • #01
TypeScript環境のNuxt.jsにVuetifyを導入しよう
2
  • Vuetifyセットアップ
  • /
  • #02
VuetifyにカスタムCSSを追加してSASS変数を理解しよう
3
  • Vuetifyセットアップ
  • /
  • #03
VuetifyにカスタムSVGアイコンを追加しよう
1
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #01
Netlify CLIをインストールして本番環境のサイトを作成しよう
2
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #02
netlify.tomlを使ってNuxt.jsをNetlifyに手動デプロイしよう
1
  • Contentfulモデル構築
  • /
  • #01
Contentfulの料金とCommunityプランの無料枠を理解する
2
  • Contentfulモデル構築
  • /
  • #02
Contentfulへ新規会員登録、ロケールの変更、API Keyの発行を行う
3
  • Contentfulモデル構築
  • /
  • #03
Contentful ブログカテゴリーモデルを作成しよう
4
  • Contentfulモデル構築
  • /
  • #04
Contentful カテゴリーモデルに1対1で関連づくblogPostモデルを作成しよう
5
  • Contentfulモデル構築
  • /
  • #05
Contentful ブログ記事に1対多で関連づくplogTagモデルを作成しよう
6
  • Contentfulモデル構築
  • /
  • #06
Contentful カテゴリー・ブログ記事・タグコンテンツを作成しよう
1
  • Nuxt.js×Contentfulセットアップ
  • /
  • #01
Nuxt.js×Contentfulセットアップ。モジュールのインストールからAPI Keyの登録まで
2
  • Nuxt.js×Contentfulセットアップ
  • /
  • #02
Contentful APIリクエストの実行 Nuxt.jsにブログコンテンツを表示しよう
3
  • Nuxt.js×Contentfulセットアップ
  • /
  • #03
ContentfulAPIをNetlifyにデプロイしよう【Nuxt FullStaticのasyncDataとfetch】
1
  • Vuex×TypeScriptセットアップ
  • /
  • #01
Vuexの型付け vuex-module-decoratorsとnuxt-typed-vuexどちらを使用するか
2
  • Vuex×TypeScriptセットアップ
  • /
  • #02
nuxt-typed-vuexのインストールとセットアップ。Vuexの型定義と呼び出し方
3
  • Vuex×TypeScriptセットアップ
  • /
  • #03
VuexにContentfulの型定義ファイルとnuxtServerInitを追加しよう
4
  • Vuex×TypeScriptセットアップ
  • /
  • #04
VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう
1
  • コンテンツページ構築
  • /
  • #01
ブログアプリのページ設計とNuxt.jsの動的ルーティングについて理解しよう
2
  • コンテンツページ構築
  • /
  • #02
カテゴリーのコンテンツページを作成しよう【Nuxt.js×Contentful】
3
  • コンテンツページ構築
  • /
  • #03
カテゴリーに関連付くブログ記事一覧を表示しよう【Nuxt.js×Contentful】
4
  • コンテンツページ構築
  • /
  • #04
injectを使用して共通エラー処理メソッドを作成しよう【Nuxt×TypeScript】
5
  • コンテンツページ構築
  • /
  • #05
NuxtChildを使用してブログ記事ページを作成しよう【Nuxt.js×TypeScript】
6
  • コンテンツページ構築
  • /
  • #06
タグ一覧ページとタグ関連づく記事一覧を表示しよう【Nuxt.js×TypeScript】
7
  • コンテンツページ構築
  • /
  • #07
プライバシーポリシーページを作成しよう【Nuxt.js×TypeScript】
8
  • コンテンツページ構築
  • /
  • #08
@nuxtjs/i18nのインストールとセットアップ。ページタイトルの翻訳化【TypeScript】
1
  • NetlifyFunctionsを使った検索機能
  • /
  • #01
Netlify Functionsを使ってクエリを返す関数を作成しよう【Nuxt.js×TypeScript】
2
  • NetlifyFunctionsを使った検索機能
  • /
  • #02
Netlify Functionsプロジェクトをデプロイしよう【Nuxt.js×TypeScript】
3
  • NetlifyFunctionsを使った検索機能
  • /
  • #03
Nuxt.js × axiosセットアップ Netlify Functionsにリクエストを行う準備をしよう
4
  • NetlifyFunctionsを使った検索機能
  • /
  • #04
オリジン•CORS•プリフライトリクエストを理解する【Nuxt.js×Netlify Functions】
5
  • NetlifyFunctionsを使った検索機能
  • /
  • #05
Netlify Functionsを使ってフォームバリデーション機能を構築しよう【Nuxt.js】
6
  • NetlifyFunctionsを使った検索機能
  • /
  • #06
ツールバーに表示する検索フォームを作成しよう【Nuxt.js×TypeScript】
7
  • NetlifyFunctionsを使った検索機能
  • /
  • #07
検索ページを作成しよう【Vue propsとTypeScriptの書き方 解説】
8
  • NetlifyFunctionsを使った検索機能
  • /
  • #08
Netlify FunctionsからContentfulAPIリクエストを送ろう【Nuxt.js】
9
  • NetlifyFunctionsを使った検索機能
  • /
  • #09
検索ページに「もっと見る」ボタンを実装しよう【Nuxt.js×TypeScript】
1
  • ブログMarkdown対応
  • /
  • #01
@nuxtjs/markdownitのインストールとセットアップ【Nuxt.js×TypeScript】
2
  • ブログMarkdown対応
  • /
  • #02
Nuxt.js×markdown-it 外部リンクを別タブで開くプラグインを追加しよう
3
  • ブログMarkdown対応
  • /
  • #03
Nuxt.js×markdown-it 内部リンクをVueRouterで高速にページ遷移しよう
4
  • ブログMarkdown対応
  • /
  • #04
Nuxt.js×markdown-it アンカーリンクとブログ目次を自動生成しよう
独学プログラマ
独学でも、ここまでできるってよ。
CONTACT
Nuxt.js制作のご依頼は下記メールアドレスまでお送りください。