どうも、花粉症のシーズンで苦しんでいるぱか@u3pakaです。
エリクサーで花粉症治らないのかな??
前回は、Absintheを用いて、Phoenix + Vue.jsのGraphQL環境まで作りました。
今回は、Vue.jsとPhoenixをGraphQLで連携させる際、状態管理をVueApolloでどのように実装するかについてまとめます。
一応、以下の記事の続きです。
|> 日曜プログラマーが錬金術やってみた!Phoenix1.4 + vue-cli の組合せ。
|> 魔酒Absintheを飲んでみる。Phoenix + AbsintheではじめるGraphQL(1)
おしながき
ぱかのアトリエ〜elixirの錬金術士〜第3回
|> プロジェクト作成
|> Vueにおける状態管理: From Axios&Vuex To VueApollo
|> VueApolloを使うときのポイント(1) queryの場合
|> VueApolloを使うときのポイント(2) mutationの場合
|> まとめ
Absinthe導入まで余裕な方は、Vueにおける状態管理: From Axios&Vuex To VueApollo まで読み飛ばしてください。
プロジェクト作成
日が空いてしまったので、前回までのおさらいです。
まずは、2019年3月4日時点でPhoenixが1.4.1になっていますので、アップデートしておきましょう。
mix archive.install hex phx_new 1.4.1
あとは、次の要領でプロジェクトを作ります(詳細説明は前々回)。
mix phx.new phx_vue --no-webpack
cd phx_vue
// config/dev.exsのパスワードを変更してから
mix ecto.create
// vue のプロジェクトを作ります(プロジェクト名はassets 変える場合は、以下の説明も各自読み替えてください。)。
// 設定はお好みでどうぞ。ただし、今回はVuexを外してください。
vue create assets
Absintheとスキーマ作成
今回もタグと文面がある簡単なモデルNoteを作り、GraphQLでアクセスできるようにします。
細かい説明はリンク先をチェック!
|> モデル作成 ->
mix phx.gen.json Post Note notes body:string tags:array:string
|> Absintheの導入 ->
def deps do
  [
    # ...
    {:absinthe, "~> 1.4"},
    {:absinthe_plug, "~> 1.4"},
  ]
end
mix deps.get
|> エンドポイントとGrpahiQLの導入 ->
defmodule PhxVueWeb.Router do
  # ...
  # scope "/api", PhxVueWeb do ... だと、スコープ設定してしまい、Absintheが読み込めない。
  # 以下に書き換える。 参考:https://stackoverflow.com/questions/45765950/absinthe-plug-not-available
  scope "/api" do
    pipe_through :api
    forward "/graphiql", Absinthe.Plug.GraphiQL,
      schema: PhxVueWeb.Schema,
      json_codec: Jason,
      interface: :simple,
      context: %{pubsub: PhxVueWeb.Endpoint}
    forward "/", Absinthe.Plug, schema: PhxVueWeb.Schema, json_codec: Jason
  end
end
http://localhost:4000/api/ でGraphQLが叩けるようにします。
|> Schemaの作成 ->
なお、前回までに作ったlib/phx_vue_web/schema.ex の完成形は以下(実際は、複数のファイルに分けます。)。
defmodule PhxVueWeb.Schema do
  use Absinthe.Schema
  alias PhxVue.Repo
  alias PhxVue.Post.{Note}
  # objectマクロでNoteを定義
  object :note do
    field :id, non_null(:id)
    field :body, non_null(:string)
    field :tags, non_null(list_of(non_null(:string)))
  end
  # queryマクロ
  query do
    @desc "Get all notes"
    field :all_notes, list_of(:note) do
      resolve(fn _parent, _args, _resolution ->
        {:ok, Repo.all(Note)}
      end)
    end
    # 以下を追加。
    @desc "Get note by id"
    field :note, :note do
      arg(:id, :id)
      # ↓ resolverの第二引数にmapとしてarg macroが渡されるので、パターンマッチ。
      resolve(fn _parent, %{:id => note_id}, _resolution ->
        Note
        |> Repo.get(note_id)
        |> case do
          nil -> {:error, "ないよ"}
          x -> {:ok, x}
        end
      end)
    end
    # queryの下に、mutation追加
    mutation do
      @desc "create new note"
      field :create_note, :note do
        arg(:body, non_null(:string))
        arg(:tags, list_of(:string))
        resolve(fn _parent, args, _resolution ->
          %Note{}
          |> Note.changeset(args)
          |> Repo.insert()
          |> case do
            {:ok, note} -> {:ok, note}
            _error -> {:error, "could not create a note"}
          end
        end)
      end
    end
  end
end
Vueにおける状態管理: From Axios&Vuex To VueApollo
さて、GraphQLを用いた場合のVueにおける状態管理を考えていきます。
フロントエンドも複雑化してくると、状態がこんがらがってきます。対処が必要です。
Vuexじゃないの?
最近ですと、React-ReduxのごとくVue-Vuexがセットで用いられます。Fluxの系列ですね。
しかし、Vuexでstate作ってあれこれ書いていると、ふとElixir側と重複したコーディングを繰り返している気がしてきます
(どうもElmにインスパイアされてFluxやRedux、Vuexが生み出されたとか。ならば、さもありなん。)。
また、GraphQLが前提にはなっていない。
Elixirとの戯れに時間を回すために、Javascriptの記述は少しでも楽になってしまいましょう。
そのために、通信部分のaxiosと状態管理のVuex部分をGraphQLのApollo-Clientに置き換えていきます。
Apolloってなに?
GraphQLに特化したクライアント。Vuex的な状態管理とAxios的な通信処理をうまいことやってくれます。
つまり、Absinthe側が状態管理をやってくれるような感覚でプログラミングできます。
Absintheのドキュメントにも、Using with Apollo Client の項目があるのですが、まだ未整備です。
まずはapollo関連パッケージをインストール。
yarn add vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-link-context apollo-cache-inmemory graphql-tag
Vueに組み込んでいきます。
import Vue from 'vue'
import App from './App.vue'
// Add
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
Vue.use(VueApollo)
const apolloProvider = new VueApollo({
  defaultClient: new ApolloClient({
    link: new HttpLink({ uri: 'http://localhost:4000/api/' }), // 初期設定値。Phoenix側のAbsinthe用URLを各自書き換えてください。
    cache: new InMemoryCache()
  })
})
//
Vue.config.productionTip = false
new Vue({
  apolloProvider,  // <- Add
  render: h => h(App)
}).$mount('#app')
VueApolloを使うときのポイント(1) queryの場合
以下2点。
1. graphql-tagを使ってgraphQLを記述
これは、graphiQLで試した出力をそのままコピペしてOKです!
import gql from "graphql-tag";
const ALL_NOTES = gql`
  query AllNotesQuery {
    allNotes {
      id
      body
      tags
    }
  }
`;
2. apolloをVueオブジェクトに追加
data(){
    return {
        allNotes: [] //graphQLで管理したいデータ名
    }
},
apollo: {
    allNotes: { // data()の変数と対応するキー
      query: ALL_NOTES,
      loadingKey: "loading"
    }
  }
これだけです。あら不思議。これだけで、Apolloがうまいこと状態管理をやってくれます。
別コンポーネントとの連携時もgqlが同じなら賢くキャッシュ等を取り扱ってくれます。
Vue componentを作ってみる
適当にNoteを表示するvueコンポーネントを作ってみます。
<template>
  <div>
    <h1>All Notes</h1>
    <ul id="all-notes">
      <li v-for="note in allNotes" :key="note.id">{{note}}</li>
    </ul>
  </div>
</template>
<script>
// ポイント1
import gql from "graphql-tag";
const ALL_NOTES = gql`
  query AllNotesQuery {
    allNotes {
      id
      body
      tags
    }
  }
`;
export default {
  data() {
    return {
      allNotes: []
    };
  },
  // ポイント2
  apollo: {
    allNotes: {
      query: ALL_NOTES,
      loadingKey: "loading"
    }
  }
};
</script>
<style>
</style>
このコンポーネントを初期画面に表示させるように、App.vueに手を加えます。
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <!-- ここ追加 -->
    <Notes></Notes>
    <!-- ここまで -->
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>
:
<script>
import Notes from "./components/Notes.vue"; //ここ追加
import HelloWorld from "./components/HelloWorld.vue";
export default {
  name: "app",
  components: {
    Notes, //ここ追加
    HelloWorld
  }
};
</script>
ElixirとVue.jsのインタラクティブな開発
Phoenixとnodejsを立ち上げます。
iex -S mix phx.server
//別枠で
yarn serve
http://localhost:8080 は次のような見た目(右側)になっているでしょう。
いいですねぇ。iex ほんと便利です(ちなみに、WSL上で動いています。)。
それでは、左のiexでデータをひとつ追加してみます。
iex(1)> alias PhxVue.Post.Note
PhxVue.Post.Note
iex(2)> alias PhxVue.Repo
PhxVue.Repo
iex(3)> %Note{} |> Note.changeset(%{:body => "first commit", :tags => ["test", "draft"]}) |> Repo.insert()
[debug] QUERY OK db=48.8ms queue=5.4ms
INSERT INTO "notes" ("body","tags","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["first commit", ["test", "draft"], ~N[2019-03-03 13:39:32], ~N[2019-03-03 13:39:32]]
{:ok,
 %PhxVue.Post.Note{
   __meta__: #Ecto.Schema.Metadata<:loaded, "notes">,
   body: "first commit",
   id: 1,
   inserted_at: ~N[2019-03-03 13:39:32],
   tags: ["test", "draft"],
   updated_at: ~N[2019-03-03 13:39:32]
 }}
無事、データが追加されています!!Apolloによるデータ取得はうまくいきました。
VueApolloを使うときのポイント(2) Mutationの場合
それでは、AbsintheのcreateNoteにつながるような簡易フォームをVue側に追加しましょう。
Notes.vueに手を加えます。
<template>
  <div>
    <!-- 以下、追加 -->
    <form>
      <textarea v-model="form.body" placeholder="body"></textarea>
      <select v-model="form.tags" multiple>
        <option>draft</option>
        <option>elixir</option>
        <option>vuejs</option>
      </select>
      <button type="button" @click="sendNote">submit</button>
    </form>
    <!-- ここまで -->
     
    <h1>All Notes</h1>
    <ul id="all-notes">
      <li v-for="note in allNotes" :key="note.id">{{note}}</li>
    </ul>
  </div>
</template>
さて、フォームが追加されました(いつもVuetify依存だったので、ドキュメントを調べながら^^;)。

このままでは、submitボタンが使えません。methodを追加します。
mutationの場合は、先程のqueryよりやや複雑になります。
ポイントは以下のコメントに書きました。
<script>
import gql from "graphql-tag";
const ALL_NOTES = gql`
  query AllNotesQuery {
    allNotes {
      id
      body
      tags
    }
  }
`;
// 新しく、CREATE_NOTEを追加
const CREATE_NOTE = gql`
  mutation createNoteQuery(
    $body: String!
    $tags: [String]
  ) {
    createNote(tags: $tags, body: $body) {
      id
      body
      tags
    }
  }
`;
export default {
  data() {
    return {
      allNotes: [],
      form: {} // フォームのデータを入れておくため
    };
  },
  apollo: {
    allNotes: {
      query: ALL_NOTES,
      loadingKey: "loading"
    }
  },
  // 追加
  methods: {
    sendNote() {
      this.$apollo // this.$apolloで呼び出せます。
        .mutate({
          mutation: CREATE_NOTE, // gqlのクエリ
          variables: this.form, // mutation gqlのクエリの引数
          refetchQueries: [`allNotes`], // 動作が終わったあとに、再読込をするgql stringで指示します。複数もOK!
          update: (store, { data: { createNote: newNote } }) => { // apollo側のキャッシュに返り値を追加します。
            const data = store.readQuery({ query: ALL_NOTES });
            data.allNotes.push(newNote);
            store.writeQuery({ query: ALL_NOTES, data });
          }
        })
        .then(data => {
          this.form = {};
        })
        .catch(error => {
          console.log(error);
        });
    }
  }
};
</script>
フォームに適当な文字を入力して、submit!
...
データが追加されていると思います。しかも、今回はupdateにあれこれ書いたために、再読込せずとも描写が変わります!
楽しくなって何度も追加する。そのたびに、iexにもメッセージが流れて楽しい。

まとめ
VueApolloを用いることでVuex要らずで状態管理ができることを示しました。
実際は、複数のコンポーネントにまたがるVue開発をするときにより大きな恩恵にあずかれます。
gql.js的なgql定数の寄せ集めファイルを作って複数のコンポーネントからimportすることで、他のコンポーネントによる状態変化を反映した動作にすることができます。
ただ今回は、ElixirというよりはむしろVueの記事になってしまいました。。。
Apollo側の記述は簡単なので、テンプレ記述することでJavaScriptに関わる時間を減らせます。
その分、Elixirに時間を費やしましょう。
こうしたVueのややこしい説明も、すべてはElixirでフロントエンドが自由に書けないから。
Phoenix LiveViewが来ると必要性が少なくなるのかな。とても楽しみです。
(ぱか)
参考
日本語文献で参考になるのはこちら。
https://qiita.com/Hiroyuki_OSAKI/items/2e0db565cfd5686eadf1
VueApolloのgithub
https://github.com/Akryum/vue-apollo
VueApolloのドキュメント。よく整備されています。これを読めば問題ない。
https://akryum.github.io/vue-apollo/guide/

