前回は画面からqueryを投げてデータを取得するところまでやってみました。今回はもうちょっと実用的な形でTODOアプリを進化させようと思います。
TL;DR
がしかし、導入したGemのバージョンが古いので、これを最新版にアップデート&最新の構成でもう一度前回と同じ仕様の実現までやってから、GraphQLをいい感じに扱えるApolloというライブラリを導入していじっていこうと思います。
前回のやり直し
前回はgemのバージョンを1.7.7指定していたのですが、今日(2019/4/15)時点では1.9.4が最新です。ジェネレータを使って生成されるファイルの内容も以前と変わっているので、最新キャッチアップをしてから先に進もうと思います。コードはこれのmasterからブランチ切ってやります。
まずはGemfileの編集とbundle installですね。
# graphql gem 'graphql', '1.9.4' gem 'graphiql-rails'
したらファイル生成
$ rails g graphql:install
すると、色々できます。
create app/graphql/types create app/graphql/types/.keep create app/graphql/todo_schema.rb create app/graphql/types/base_object.rb create app/graphql/types/base_enum.rb create app/graphql/types/base_input_object.rb create app/graphql/types/base_interface.rb create app/graphql/types/base_scalar.rb create app/graphql/types/base_union.rb create app/graphql/types/query_type.rb add_root_type query create app/graphql/mutations create app/graphql/mutations/.keep create app/graphql/types/mutation_type.rb add_root_type mutation create app/controllers/graphql_controller.rb route post "/graphql", to: "graphql#execute" gemfile graphiql-rails route graphiql-rails
ルーティングは自動で前回と同じ内容になりますが、types以下の構成がだいぶ変わりましたね。
一旦graphiqlから動作確認してみましょう。
バッチリ。
続いて、Taskの型ファイルも生成しましょう。
rails g graphql:object Task id:ID! title:String! status:Int! priority:Int!
できたのはこちら。
module Types class TaskType < Types::BaseObject field :id, ID, null: false field :title, String, null: false field :status, Integer, null: false field :priority, Integer, null: false end end
Rubyになりましたね。次は最初のタスクを取得するのとIDで検索するQueryを定義してみます。
module Types class QueryType < Types::BaseObject field :first_task, TaskType, null: false, description: "return first task" field :task_find_by, TaskType, null: false, description: "return a task" do argument :task_id, Integer, required: true end def first_task Task.first end def task_find_by(task_id:) Task.find_by(id: task_id) end end end
書き方もぼちぼち変わりましたね。個人的にはこっちの方が親しみを感じます。
field名はキャメルケースでもスネークケースでもいいみたいです。Query投げる時はキャメルケースじゃないとだめみたいでした。
以前はfield内にresolverを書いていましたが、今回はデフォルトでは同名のメソッドがresolverとして動作するみたいです。resolver_method:というオプションをフィールドに渡すとメソッドの指定ができます。こんな感じですね。
field :resolver_method_exam, String, null: false, description: "this is resolver sample", resolver_method: :say_hello def say_hello "Hello, resolver!" end
というわけで前回やったとこまで追いついたので、これを楽に扱うライブラリ、Apolloを導入します。
導入しないとどうなるか
適当にボタンの設置とcoffeeでajax。
button#first-task-button first_task
$ -> $('#first-task-button').on 'click', -> query = {query: '{firstTask {id title status priority}}'}; $.ajax url: '/graphql', type: 'POST', dataType: 'json', data: query .done (result) -> console.log(result) .fail (error) -> console.log(error)
なんか嫌ですね。これを無限に定義しないといけないとなると、悲しい未来が見えます。
なので、クライアントサイドから楽にGraphQLを扱うためのライブラリ、Apolloを使ってみます。あと色々と楽なのでVueも入れます。
VueとApollo導入する
webpacker入れてvue導入
gem 'webpacker'
rails webpacker:install
rails webpacker:install:vue
/app/javascript/packs/vue_apollo_sample.jsを作成
import Vue from 'vue/dist/vue.esm.js' document.addEventListener('DOMContentLoaded', () => { const app = new Vue({ el: '#mount_target', data: { message: 'hello, vue' } }) })
ついでにGraphQLで色々試すためのページを作りましょうか。
routes.rb
resources :tasks do collection do get :graph, to: 'tasks#graph_index' end end
tasks_controller.rb
# 追加 def graph_index end
graph_index.html.slim
= javascript_pack_tag 'vue_apollo_sample' #mount_target p = "{%raw%}{{message}}{%endraw%}"
これで、今作ったページでVueが動作するようになっているはずです。/tasks/graphにhello, vueが表示されていればOK。
したらApolloの導入ですね。vue向けのライブラリも一緒に入れましょう。
yarn add graphql graphql-tag apollo-client apollo-boost vue-apollo
それぞれどんなものかざっくり。
- garphql: graphqlをjsで扱うためのもの
- graphql-tag: クエリを楽に投げるための便利ツール
- apollo-client: apollo本体
- apollo-boost: apollo-clientを簡単に扱うためのラッパー
- vue-apollo: vue向け統合ライブラリ
ではいざ!
Apollo実装編
基本的にapollo-boostに乗っかっていきます。
Vue×Apolloなところ...vue_apollo_sample.js
import Vue from 'vue/dist/vue.esm.js' import ApolloClient from "apollo-boost"; import VueApollo from "vue-apollo"; import { gql } from "apollo-boost"; const client = new ApolloClient({ uri: "http://localhost:3008/graphql", request: async operation => { operation.setContext({ headers: { 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').getAttribute('content'), }, }); } }); const apolloProvider = new VueApollo({ defaultClient: client }); Vue.use(VueApollo); document.addEventListener('DOMContentLoaded', () => { // graphql-tagが活躍しているところ const ALL_TASK_QUERY = gql` query allTask{ allTask { id title status priority } }` // 引数ありなクエリ const SEARCH_TASK_QUERY = gql` query taskSearchBy($taskName: String!){ taskSearchBy(taskName: $taskName) { id title status priority } }` const app = new Vue({ el: '#mount_target', // これを渡すことで、this.$apolloからクエリを投げることができるようになる apolloProvider: apolloProvider, mounted() { self = this; this.$apollo.query({ query: ALL_TASK_QUERY }) .then(function (result) { self.tasks = result.data.allTask; }) .catch(function (error) { console.log(error); }); }, data: { tasks: [], }, methods: { search: function (e) { this.$apollo.query({ query: SEARCH_TASK_QUERY, variables: { taskName: e.target.value } }) .then(function (result) { self.tasks = result.data.taskSearchBy; }) .catch(function (error) { console.log(error); }); } } }) })
クエリ検索の仕様を、idからtitleの部分一致に変えました。
query_type.js
module Types class QueryType < Types::BaseObject field :all_task, [TaskType], null: false, description: "return all task" field :first_task, TaskType, null: false, description: "return first task" field :task_search_by, [TaskType], null: false, description: "return a task" do argument :task_name, String, required: true end field :resolver_method_exam, String, null: false, description: "this is resolver sample", resolver_method: :say_hello def all_task Task.all end def first_task Task.first end def task_search_by(task_name:) Task.where('title like ?', "%#{task_name}%") end def say_hello "Hello, resolver!" end end end
viewはdataのtasksを全件表示したり検索input置いたりしてます。入力されるたびに検索走ります。結果はApolloがキャッシュしてくれます。
graph_index.html.slim
= javascript_pack_tag 'vue_apollo_sample' #mount_target b search: input[type="text" @input="search"] div[v-for="task in tasks" :key="task.id"] = "{%raw%}{{task.title}}{%endraw%}"
すると、こうなる
う、動いたーーー! Vueを導入したのもありますが、jQueryで頑張るパターンと比較してなにかつらみから解放された感がありますね。
今回はサンプルということもあり1ファイルのjsで頑張りましたが、クエリ自体は別ファイルで管理するとか、Apolloのproviderは親コンポーネント/処理は子コンポーネントという構成にしたりとか、もっと人道的な工夫はできます。あと、apollo-boostはカスタマイズには向かないので、業務で使うならapollo-clientでしっかり設定していくべきとの言説を見かけました。
とかとかありますがひとまず動いた!達成感!