Top View


Author Yuhei Okazaki

Nuxt.js + GraphQL + Ruby on Railsで作るToDoアプリチュートリアル(後編)

2019/08/25

ソースコード

以下に公開しています。 バックエンド側は前編で実装済みです。

今回はフロントエンド側を実装します。

バックエンド側

フロントエンド側

動作環境

最新版のNode.jsをインストールした状態からスタートします。

$ node -v
v12.9.0

Nuxt.jsのプロジェクトを作成

create-nuxt-appを使い、 rails_nuxt_grapshql_todoapp_front という名前のプロジェクトを作成します。
以下のようにコマンドを実行し、選択していきます。

$ npx create-nuxt-app rails_nuxt_grapshql_todoapp_front                                                                                                                                                                [~]
create-nuxt-app v2.8.0
  Generating Nuxt.js project in /Users/yokazaki/rails_nuxt_grapshql_todoapp_front
? Project name rails_nuxt_grapshql_todoapp_front
? Project description This is a tutorial for creating a ToDo app.
? Author name Yuhei Okazaki
? Choose the package manager Npm
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Progressive Web App (PWA) Support
? Choose linting tools ESLint
? Choose test framework None
? Choose rendering mode Single Page App

実行し終わったら早速起動してみましょう。

$ cd rails_nuxt_grapshql_todoapp_front
$ npm run dev

http://localhost:3000 へアクセスすると初期画面が表示されます。

apollo-moduleの追加

今回、GraphQLのクライアントとしてapollo-moduleを使用します。
以下コマンドでインストールできます。

$ npm install --save @nuxtjs/apollo
$ npm install --save graphql-tag

vuetify-moduleの更新

今回、デザインコンポーネントとしてVuetifyを使っていますが、create-nuxt-app でインストールされるvuetify-moduleのバージョンがかなり古いものであったため更新しておきます。

$ npm install --save @nuxtjs/vuetify@v1.3.3

nuxt.config.jsを追記

nuxt.config.jsを追記します。
追記内容としては「apollo-moduleの設定」と「ホスト名の設定(ポート番号)」の2点です。
ホスト名は前編の最後に行ったCORS設定に合わせています。

import colors from 'vuetify/es5/util/colors'

export default {
  mode: 'spa',
  /*
  ** Headers of the page
  */
  head: {
    titleTemplate: '%s - ' + process.env.npm_package_name,
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      {
        rel: 'stylesheet',
        href:
          'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons'
      }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },
  /*
  ** Global CSS
  */
  css: [
  ],
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
  ],
  /*
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/vuetify',
    '@nuxtjs/pwa',
    '@nuxtjs/eslint-module',
    '@nuxtjs/apollo' // ★追加
  ],
  /*
  ** vuetify module configuration
  ** https://github.com/nuxt-community/vuetify-module
  */
  vuetify: {
    theme: {
      primary: colors.blue.darken2,
      accent: colors.grey.darken3,
      secondary: colors.amber.darken3,
      info: colors.teal.lighten1,
      warning: colors.amber.base,
      error: colors.deepOrange.accent4,
      success: colors.green.accent3
    }
  },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend(config, ctx) {
    }
  },
  // ★ ここから追記
  apollo: {
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:3000/graphql'
      }
    }
  },
  server: {
    port: 8080
  }
  // ★ ここまで追記
}

GraphQL QueryとMutationを定義

apollo-moduleが使用するGraphQL QueryとMutationをファイルに記述していきます。

apollo/queries/tasks.gql

query {
  tasks {
    id
    title
    description
    completed
  }
}

apollo/mutations/createTask.gql

mutation($title: String!, $description: String!) {
  createTask( input: { title: $title, description: $description }) {
    task {
      id
      title
      description
      completed
    }
  }
}

apollo/mutations/updateTask.gql

mutation($id: ID!, $completed: Boolean!) {
  updateTask( input: { id: $id, completed: $completed }) {
    task {
      id
      title
      description
      completed
    }
  }
}

apollo/mutations/deleteTask.gql

mutation($id: ID!) {
  deleteTask( input: { id: $id }) {
    task {
      id
      title
      description
      completed
    }
  }
}

ページを実装

ページのデザインおよびTaskの作成、更新、削除処理を実装します。
まずは、layouts/default.vueを編集して、ヘッダー・フッターのデザインを整えます。

<template>
  <v-app>
    <v-toolbar fixed>
      <v-toolbar-title v-text="title" />
    </v-toolbar>
    <v-content>
      <nuxt />
    </v-content>
    <v-footer center>
      <v-layout justify-center>
        <span>&copy; 2019 Yuhei Okazaki. All Rights Reserved.</span>
      </v-layout>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data() {
    return {
      title: 'Tasks'
    }
  }
}
</script>

次にpages/index.vueを編集します。

<template>
  <v-container>
    <v-row>
      <v-col cols="6" offset="3">
        <v-card>
          <v-card-title>Register Task</v-card-title>
          <v-card-text>
            <v-form>
              <v-container>
                <v-row>
                  <v-col cols="6">
                    <v-text-field v-model="newTask.title" label="Title" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col cols="12">
                    <v-text-field v-model="newTask.description" label="Description" required />
                  </v-col>
                </v-row>
                <v-row>
                  <v-col>
                    <v-btn @click="createTask(newTask)">
                      Register
                    </v-btn>
                  </v-col>
                </v-row>
              </v-container>
            </v-form>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
    <v-row v-for="task in tasks" :key="task.id">
      <v-col cols="6" offset="3">
        <v-card>
          <v-card-title>
            <v-checkbox v-model="task.completed" :label="task.title" @click="updateTask(task)" />
            <v-btn text icon @click="deleteTask(task)">
              <v-icon>clear</v-icon>
            </v-btn>
          </v-card-title>
          <v-card-text>{{ task.description }}</v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<script>
import tasks from '~/apollo/queries/tasks'
import createTask from '~/apollo/mutations/createTask'
import deleteTask from '~/apollo/mutations/deleteTask'
import updateTask from '~/apollo/mutations/updateTask'

export default {
  data() {
    return {
      tasks: {},
      newTask: {
        title: '',
        description: ''
      }
    }
  },
  methods: {
    async createTask(task) {
      try {
        await this.$apollo.mutate({
          mutation: createTask,
          variables: {
            id: task.id,
            title: task.title,
            description: task.description
          },
          refetchQueries: [{
            query: tasks
          }]
        })
        this.newTask = { title: '', description: '' }
      } catch (e) {
        window.console.log(e)
      }
    },
    async deleteTask(task) {
      try {
        await this.$apollo.mutate({
          mutation: deleteTask,
          variables: {
            id: task.id
          },
          refetchQueries: [{
            query: tasks
          }]
        })
      } catch (e) {
        window.console.log(e)
      }
    },
    async updateTask(task) {
      try {
        await this.$apollo.mutate({
          mutation: updateTask,
          variables: {
            id: task.id,
            completed: !task.completed
          },
          refetchQueries: [{
            query: tasks
          }]
        })
      } catch (e) {
        window.console.log(e)
      }
    }
  },
  apollo: {
    tasks: {
      query: tasks
    }
  }
}
</script>

ソースコードの内容を大まかに解説します。
コードが少し長いですが複雑な処理はしていないので、コードと照らし合わせて理解してみてください。

  • 保持するデータはタスク一覧( tasks )と新規登録用タスク( newTask )のみ
  • ページ表示時点で tasks のQueryを実行して、タスク一覧をフェッチする
  • フォームのRegisterボタンをクリックしたときに createTask() を呼び出し、 createTask のMutationを実行、その後 tasks Queryを再実行する
  • タスクのチェックボックスを操作すると updateTask() を呼び出し、 updateTask のMutationを実行、その後 tasks Queryを再実行する
  • タスクの☓ボタンをクリックすると deleteTask() を呼び出し、 deleteTask のMutationを実行、その後 tasks Queryを再実行する

動作確認

以上で実装は完了なので、動かしてみましょう!

まず、バックエンド側を起動します。

// バックエンドのプロジェクトへ移動
$ cd rails_nuxt_grapshql_todoapp // 移動先パスは各自の環境に合わせてください
$ rails s

次に別のTerminalでフロントエンド側を起動します。

// フロントエンドのプロジェクトへ移動
$ cd rails_nuxt_grapshql_todoapp_front // 移動先パスは各自の環境に合わせてください
$ npm run dev

http://localhost:8080へアクセスするとToDoアプリが表示されます。
お疲れ様でした。

まとめ

本チュートリアルではバックエンド側にRuby on Rails、フロントエンド側にNuxt.jsを用いて、GraphQLによるデータの同期を実現することでToDoアプリを作成しました。

これまでフロントエンドに詳しくない人でも、ToDoアプリを作ったことである程度仕組みを理解し、自信を得られたかと思います。

バリデーションを追加したり、ドラッグ・アンド・ドロップで並び替えられるようにしたり、ユーザのログイン/ログアウト機能を付けたりとまだまだ実装の余地はありますのでぜひチャレンジしてみてください。
(これらは、需要があれば別の記事で解説したいと思います)

Yuhei Okazaki

Yuhei Okazaki

Twitter X

2018年の年明けに組込み畑からやってきた、2児の父 兼 Webエンジニアです。 mockmockの開発・運用を担当しており、組込みエンジニア時代の経験を活かしてデバイスをプログラミングしたり、簡易的なIoTシステムを作ったりしています。主な開発言語はRuby、時々Go。