Blogブログ

nuxt3

vue

graphql

2023.02.05

エジプトからVue3.jsのNuxt3とgraphqlで開発を解説してみます。

こんにちは。

今エジプトのナイル川の中洲のソフィテルホテルにいます。

前回思った以上にnuxt3の記事の反響があったので、
実際の開発をもう少し書かないとやばいなと思いまして、
前のブログの続きを書こうと思います。
こちらのブログ見る前に

こちらのvue3 nuxt3 graphqlで実装経験のある方のブログ
Nuxt 3 のベータ版が安定版に突入!Nuxt 3 実装経験のあるエンジニアが最新の Nuxt 3 アップデート内容をまとめます
を見た方が用語等でわかりやすいかもです。
(弊社はwebエンジニアの転職におすすめな転職サイトを紹介しています。)

まずいつもの運営から何を話してもいいと言われている介護のパークのリニューアルの実例を参考に解説していきます。

docker-compose.ymlはこんな感じになります。

version: "3"
volumes:
  php-fpm-socket:
  pgdata:
services:
  postgres:
    image: postgres:13.4
    ports:
      - 5432:5432
    volumes:
      - pgdata:/var/lib/postgresql/data:delegated
    environment:
      POSTGRES_USER: kaigo_dev
      POSTGRES_PASSWORD: kaigo_dev
      PGPASSWORD: kaigo
      POSTGRES_DB: kaigo_kyujin_park_dev
      TZ: "Asia/Tokyo"

  pgadmin4:
    image: dpage/pgadmin4:6.11
    ports:
        - 8888:80
    volumes:
        - ./db/pgadmin4:/var/lib/pgadmin:delegated
    environment: # Set the login information of pgAdmin 4.
        PGADMIN_DEFAULT_EMAIL: root@root.com
        PGADMIN_DEFAULT_PASSWORD: root
    hostname: pgadmin4
    container_name: pgadmin4
    depends_on:
      - postgres
    restart: always


  php:
    build: ./php
    volumes:
      - ./apps:/var/www/html:delegated
      - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini
      - ./php/php-fpm.d/zzz-docker.conf:/usr/local/etc/php-fpm.d/zzz-docker.conf
      - type: volume
        source: php-fpm-socket
        target: /var/run/php-fpm
        volume:
          nocopy: true
    environment:
      DB_CONNECTION: pgsql
      DB_HOST: postgres
      DB_PORT: 5432
      DB_DATABASE: kaigo_kyujin_park_dev
      DB_USERNAME: kaigo_dev
      DB_PASSWORD: kaigo_dev
    depends_on:
      - postgres
      - mailhog

  nginx:
    image: nginx:latest
    volumes:
      - ./nginx/conf.d/nginx.conf:/etc/nginx/nginx.conf
      - ./nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf
      - ./apps:/var/www/html
      - type: volume
        source: php-fpm-socket
        target: /var/run/php-fpm
        volume:
          nocopy: true
    # restart: always
    ports: ["80:80"]
    depends_on: ["php"]

  mailhog:
    image: mailhog/mailhog:latest
    ports:
      - "1025:1025"
      - "8025:8025"
    networks:
      - default

  redis:
    image: redis:latest
    volumes:
      - ./redis/data:/data
    ports:
      - 6379:6379

バックエンドの構築をしていきます。

■ dockerコンテナの立ち上げ

$ docker-compose up -d

■ composer install

$ docker-compose exec php composer install

■ Storage

$ docker-compose exec php php artisan storage:link

■ .envの作成

$ cd app && cp .env.example .env

■ キー発行

$ docker-compose exec php php artisan key:generate

■ postgresへデータをダンプ

// SQLファイルが圧縮されている場合は下記を実行して解凍
$ gunzip kaigo_kyujin_park.sql.gz

$ docker-compose exec -T postgres pg_restore -U kaigo_dev -d kaigo_kyujin_park_dev < kaigo_kyujin_park.sql

■ /etc/hostの編集

// 下記を追記

127.0.0.1 local.5159289.jp

DEV コマンド

migration 実行

$ docker-compose exec php php artisan migrate

migrationファイル作成

$ docker-compose exec php php artisan make:migration xxxxxx

開発環境URL

Host

local.5159289.jp

Admin

ttp://local.5159289.jp/admin

Graphql Play Ground

ttp://local.5159289.jp/graphiql

はい。ここまでは良いですね。
私も市場を通ってピラミッドに移動します。

そしてフロントエンドを構築していきます。

Setup

#graphQL入れます
$ yarn -D add @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typescript-vue-apollo @vue/apollo-composable graphql typescript @graphql-codegen/typescript @graphql-codegen/typescript-graphql-request
$ yarn -D add @nuxtjs/apollo@next

# yarn
$ yarn install

# npm
$ npm install

# pnpm
$ pnpm install

Development Server

http://localhost:3000

yarn devを実行

これで準備はできましたね

ではまずは、先ほどyarn -D addのコマンドで、graphqlのライブラリをインストールしたので、使ってみましょう。

設定を作っていきます。

設定ファイルはcodegen.ymlの中に書いていきます。
(このymlは自分で作るものです)

このファイブラリはgraphqlの記述があるコードから、
laravel側へqueryを投げるための関数を生成する役割です。

■schema とは graphqlのAPIのエンドポイントを書きます。
■documentsとはgraphqlファイルが置かれているパスを記載します。
■generatesはgraphql code genで生成されたコードをどこに設置するかです。

ではdocumentsの中にgraphqlの記述を書いていきましょう。

このファイルのコードは、

query Jobs(
  $client_id: ID
  $prefecture_ids: [ID]
  $city_ids: [ID]
  $employment_type_codes: [ID]
  $merit_codes: [ID]
  $job_type_small_codes: [ID]
) {
  Jobs(
    prefecture_ids: $prefecture_ids
    city_ids: $city_ids
    client_id: $client_id
    employment_type_codes: $employment_type_codes
    merit_codes: $merit_codes
    job_type_small_codes: $job_type_small_codes
  ) {
    data {
      id
      job_pr
      client {
        id
        name
        url
        policy
        advantage
        eudcation_human_resources
      }
      prefecture {
        id
        name
      }
      distriction {
        id
        name
      }
      meritCodes {
        code
        merit_category_code
        name
        merit_display_name
        meritCategoryCode {
          code
          name
        }
      }
      jobTypeSmallCode {
        code
        name
      }
      employmentTypeCode {
        code
        name
      }
      job_pict_0
      job_pict_1
      job_pict_2
      job_pict_3
      job_pict_4
      access
      transportation_fee
      content
      training_description
      experience
      qualification
      required_personality
      suitable_personality
      member_features
      atmosphere
      nursery_advantage
      advantage
      map_url
      salary_description
      working_description
      earlytime_working
      daytime_working
      latetime_working
      nighttime_working
      working_time_description
      welfare
      holidays
      selection_flow
      valid_chk
    }
  }
}

query Job($id: ID!) {
  Job(id: $id) {
    id
    job_pr
    client {
      id
      name
      url
      policy
      advantage
      eudcation_human_resources
    }
    prefecture {
      id
      name
    }
    distriction {
      id
      name
    }
    meritCodes {
      code
      merit_category_code
      name
      merit_display_name
      meritCategoryCode {
        code
        name
      }
    }
    jobTypeSmallCode {
      code
      name
    }
    employmentTypeCode {
      code
      name
    }
    job_pict_0
    job_pict_1
    job_pict_2
    job_pict_3
    job_pict_4
    access
    transportation_fee
    content
    training_description
    experience
    qualification
    required_personality
    suitable_personality
    member_features
    atmosphere
    nursery_advantage
    advantage
    map_url
    salary_description
    working_description
    earlytime_working
    daytime_working
    latetime_working
    nighttime_working
    working_time_description
    welfare
    holidays
    selection_flow
    valid_chk
  }
}

この長いコードの解説をします。

ただその前に、

ピラミッドとスフィンクスの横で休憩してから解説します。

はい。

では先ほどのコード、
冷静にみると、

query Jobs( ■変数名■) {
Jobs(
■where句■
) {
■select文■
}
}
という形になっています。


変数名■のところは
$client_id: ID
$prefecture_ids: [ID]
とありますが、
これはこれからwhere句やselect文でこのような変数を使っていくよということと
その型を設定しています。[ID]ならIDが配列で来ることを示しています。

そして、Jobs(■where句■)のところは

prefecture_ids: $prefecture_ids
city_ids: $city_ids

と、ありますが、これはそぞれのカラムに値をセットしています。

■select■のところは、
data {
id
job_pr
client {
id
name
url
policy
advantage
eudcation_human_resources
}
}
のように書いてありますが、
これは普通に取得するカラムを指定しているだけです。
ページネーション情報等はdataの中のid等と同階層に持たせる予定です。

このようにしてgraphqlをnuxt3に書いたとします。

今回graphqlクライアントとしてapolloを使います。
(axiosのようなものだと思ってください)
apolloに関してはこのブログが多分私のブログよりも詳しいです(まだ見てない)

apolloを使えるようにするためにも設定をまた記載します。

apollo: {
    clients: {
      default: {
        httpEndpoint:
          process.env.GRAPHQL_END_OPOINT || 'http://local.5159289.jp/graphql',
        httpLinkOptions: {
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
          },
        },
      },
    },
  },

nuxt.config.tsにこれを追記するとapolloが使えるようになります。

わかりますね。

では早朝のクフ王の墓の上でmacを広げて続きをやっていきます。

これから、graphql code genとapolloを使って関数を作っていきます。
どうやって作るかというと、
ターミナルでfrontendファイルのルートで、

$ yarn graphql-codegen

を実行します。
そうすると、graphqlのファイルを読み込んで
サーバーサイドのAPIと一回通信して、通信時にそれぞれの変数に入ってきた情報の型を自動的にみてくれて、
型情報も定義された関数をgeneratedフォルダに作ってくれます。

はい、ここにできてきます。

ではここに生成された関数を実際にpageから呼び出してみましょう。

pages/jobs/practice.vueを作りました。
画像の中の解説を見るとわかりやすいと思います。

ではこのpractice.vueをSSRにしてみます。

せっかく書いた上のコードを全部↓このように変えてください

<script setup lang="ts">
import { useJobsQuery } from '~~/generated/operations'
//
import type { JobsQuery } from '~~/generated/operations'

type JobsDataType = JobsQuery['Jobs']
//useFetchの場合、エンドポイントがURLとなる。
//今回はgraphQLなので、graphql code genが生成した関数から取得するので、
//そういう時はuseAsyncDataを使う(RESTのようにURLから撮るときはuseFetch)
const { data } = await useAsyncData('JobListData', async () => {
  //useAsyncDataを使って取得したデータはキャッシュされる、そのキーが第一引数にセットされる
  //第二引数で渡されたasync関数の中で、データを取得してjobsDataに入れている
  const { result: jobsData, loading } = useJobsQuery()
  return {
    jobsData: jobsData.value as JobsDataType,
    loading: loading,
  }
})
</script>

<template>
  <div>
    <h1>求人一覧</h1>

    <div v-if="data?.loading">求人読み込み中</div>
    <div v-else>
      <ul>
        <li v-for="job in data?.jobsData?.data" :key="job.id">
          <NuxtLink :to="`/sample/jobs/${job.id}`">
            <div>
              <h2>{{ job.job_pr }}</h2>
              <p>ID: {{ job.id }}</p>
            </div>
          </NuxtLink>
        </li>
      </ul>
    </div>
  </div>
</template>

<style lang="sass"></style>

コードに解説を書きました。

せっかく書いたコードを全部変えることになったので猫型の布をラクダにあげてストレス解消しておきました。

こちらのリプレイスしたコードは、
vue3 nuxt3 graphqlの組み合わせなので、
SSRにするためにはuseFetchでデータを持ってくるか、
useAsyncDataでデータを持ってくるか、
いずれかをすればSSRで取得できます。
これは関数の実行タイミング故です。

RESTのようにエンドポイントがURLのようになってるならuseFetchでいいのですが、そうではない場合は、useAsyncDatasを使ってデータを持ってきましょう。

関係ないですが、vue3 nuxt3 graphqlのyoutubeでこの動画が一番楽しそうでした

状態管理していきます。

piniaというモジュールを使って状態管理していきます。
フロントエンドのルートで、
これを実行します。

yarn add @pinia/nuxt

インストールしたらpiniaの設定をnuxt.config.jsで行っていきます。
nuxt.config.jsのmoduleの中に使うモジュールたちを配列で記述していくので、
今回はapolloやimage-edgeも使うこともありこのように書きます。

 modules: ['@nuxtjs/apollo', '@nuxt/image-edge', '@piniaunuxt'],

全体のコードとしては

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  modules: ['@nuxtjs/apollo', '@nuxt/image-edge', '@piniaunuxt'],

  runtimeConfig: {
    public: {
      cookieSessionName: process.env.COOKIE_SESSION_NAME,
    },
  },
  
  typescript: {
    shim: false,
    strict: true,
    typeCheck: true,
  },

  image: {
    // Options
  },

  apollo: {
    clients: {
      default: {
        httpEndpoint:
          process.env.GRAPHQL_END_OPOINT || 'http://local.5159289.jp/graphql',
        httpLinkOptions: {
          credentials: 'include',
          headers: {
            'Content-Type': 'application/json',
          },
        },
      },
    },
  },
})

こうなります。

次にrootの直下にstoreというフォルダを作ります。
今回、求人関係のデータを保持したいのでjob.tsを作ります。

このjob.tsのコードはこうなっています

import { defineStore } from 'pinia'
#graphqlを実行する関数をインポートしてる
import { useJobsQuery } from '~~/generated/operations'
#JobsQueryの型を持ってきてる
import type { JobsQuery } from '~~/generated/operations'

type JobListType = JobsQuery['Jobs']

//第一引数はこの状態管理のキー名(ここではjobListStore)
export const useJobListStore = defineStore('jobListStore', {
//このdefineStoreの関数は
//state , action gettersを設定することができる(今のコードはgetterは書いていない)
  state: () => ({

    //jobsはjobListTypeの型ですよと定義してる書き方
    //初めはundefinedが入ってて、値が入ってくるならJobListTypeの型でないといかんですよという設定です。型を設定するとVSコードを使用してる場合にこのオブジェクトを使う場合に何のキーがそのオブジェクトの中にあるのか等を表示してくれるので便利です
    jobs: undefined as JobListType,
    loading: true,
  }),

  actions: {

//上で定義したstateのjobsを書き換えるための関数
    async fetchJobList() {

      //まずjsの性質として{x:[x1:1,x2:2],y:[y1:1,y2:2]} という結果を返すオブジェクトのtestというものがある時に、
      const {x , y} = testと書くと、
      xには[x1:1,x2:2]
      yには[y1:1,y2:2]が入ってくるという性質がある。
            その上で、
      useJobsQuery()の返り値にはloadingというキー名でboolian値が返ってくる
      //第一引数の loading:XXXのloadingはロード中にはtrueが返ってくる仕様であるuseJobsQueryのboolianを受け取るために書かれている。
            //useJobsQueryが実行されているときはloadingの中にtrueが返され続ける
            //そして実行が終わるとfalseが返ってくる性質がある。
            //ただ、それをloadingという変数ではなくて他の変数として受け取りたい時は
      //loading: xxx として書くと、このxxxにtrueやfalseが返ってきてくれる

      //onResultは関数の実行が終わった後に実行される
      //第二引数のこのonResultはonResultという名前でないといけない。
      const { loading: jobListLoading, onResult } = useJobsQuery()
      this.loading = jobListLoading.value
      
      //userJobsQueryの結果はresultに入ってくる
      onResult((result) => {
        this.jobs = result.data.Jobs
        this.loading = jobListLoading.value
      })
    },
  },
})

では、このfetchJobList()を使って求人一覧ページを作っていきましょうか。

コードの中にめちゃめちゃ解説書きました。

<script setup lang="ts">
import { useJobListStore } from '~~/store/job'

//importされたuseJobListStoreをインスタンス化してるようなイメージ
const jobListStore = useJobListStore()

//useAsyncDataはNuxt3の標準の関数で、SSRでレンダリング前に実行される関数
//第一引数のJobListDataは結果を受け取るキーとして指定される
await useAsyncData('JobListData', async () => {
  jobListStore.fetchJobList()
  return 'Done'
})
</script>

<template>
  <div>
    <h1>求人一覧</h1>

    <div v-if="jobListStore.loading">求人読み込み中</div>
    <div v-else>
      <ul>
                  <!--  ?は、jobsがundefinedだったら無視してundefied出なかったらforを回す書き方 -->
        <li v-for="job in jobListStore.jobs?.data" :key="job.id">
          <div>
            <h2>{{ job.job_pr }}</h2>
            <p>ID: {{ job.id }}</p>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<style lang="sass"></style>

これで、graphqlの設定をcodegen.ymlの設定を書いた上で、
yarn graphql-codegenを実行し、
generatedフォルダの中に実行用の関数を自動生成しておき、

非同期通信をするために、
storeフォルダの中のjob.tsの中で、piniaを使って、
その生成された関数を呼び値を呼び出す関数を定義しておき、

それをpagesから呼び出して使うということができました。

vue3 nuxt3 grapjhqlの技術としてはこちらのgithubが良さそうなコードなんですかね。。
でもこれまで解説したことが最低限わかっていないと「で、何からやったらいいの?」ってなってしまうと思います。
■最後にnuxt3 graphqlをテーマにして書いてるきょんさんという方のブログが分かりやすくていいなと思ったのと、
実際にちょっとnuxt3とgraphqlを書いて学びたい人向けにファブル01さんのブログがとても良かったので共有します。

最後にまたちょっと疲れたので軽く飲んで寝ます。

では。