🗂

俺でもわかるGraphQLでsvelte-apolloクライアント(4)

2021/05/30に公開

はじめに

いやー、気候がいいので週末はBBQやピザ作りにかまけてほったらかしになってしまっていましたが、そろそろクライアント側もやってみようと思います。前回までで、サーバー側が一通り動いたので、これと繋がるクライアントを作ってみます。

Apollo公式サイトでは、クライアントはReact以外は知らん、というオーラ満開で悲しいですが、ここはあえてSvelteでTODOアプリのクライアントつくってみます。

TL;DR

  • SvelteとApolloでGraphQLのクライアントを動かしてみた
  • 普通に@apollo/clientでもできるが、Apollo公式でも紹介されている、svelte-apolloがなかなかよかった。svelte-apolloはドキュメント少なめ[1]なので、こんなのあったらよかったのになっていうのを書く
  • Svelteはタスクランナーはrollup推し。rollupでちょいハマったところも書く

SvelteとApollo Clientの準備

こんなプロジェクト構成にしていく

svelte-apollo-todo/
├── apollo 
└── svelte 

Svelte入れる

cd svelte-apollo-todo/
npx degit sveltejs/template svelte
cd svelte
npm install

GraphQL関係を入れる

npm i --save @apollo/client graphql

ApolloとSvelteを起動

ここで、App.svelteを以下のようにすればとりあえず動きます。ここではgqlがGraphQLの構文を解釈し、JSモジュールにしてくれている。

App.svelte
import { InMemoryCache, ApolloClient, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache()
});

client
  .query({
    query: gql`
      query list {
        tasks {
          id
          name
          isActive
          createdAt
          updatedAt
          owner
        }
      }
    `
  })
  .then(result => console.log(result));

レスポンスはこんな感じ

{data: {}, loading: false, networkStatus: 7}
  data:
    tasks: Array(4)
        0: {__typename: "Task", id: "1", name: "Soak in an Onsen", isActive: true, createdAt: null,}
        1: {__typename: "Task", id: "2", name: "Sing Karaoke", isActive: false, createdAt: null,}
        2: {__typename: "Task", id: "3", name: "See cherry blossom", isActive: true, createdAt: null,}
        3: {__typename: "Task", id: "4", name: "Buy some milk", isActive: true, createdAt: 1621812683,}
    length: 4
      __proto__: Array(0)
    __proto__: Object
  loading: false
  networkStatus: 7
  __proto__: Object

トップレベルに、loading: falseとかあるので、これで読み込み中 or 読み込み完了などの制御もできそうだ。まあ、このやり方でもいいのだが、軽く探してみると、Svelteとのインテグレーションを楽にしてくれそうなsvelte-apolloというのがあるみたいなので、そっちでやってみることにします。

svelte-apolloの場合

https://github.com/timhall/svelte-apollo

インストールする

cd svelte
npm i --save svelte-apollo @rollup/plugin-graphql @rollup/plugin-replace

この時点で、package.jsonの各種バージョンはこんな感じ

  "dependencies": {
    "@apollo/client": "^3.3.19",
    "@rollup/plugin-graphql": "^1.0.0", // こっちのローダーを使う
    "graphql": "^15.5.0", // 今後はこのローダー使わない
    "sirv-cli": "^1.0.0",
    "svelte-apollo": "^0.4.0"
  }

TODOのGraphQLバックエンドに対する一通りの操作をするApp.svelteはこんな感じ

App.svelte
import { InMemoryCache, ApolloClient } from '@apollo/client'; // ❶
import { setClient, query, mutation } from "svelte-apollo"; // ❸
import { list_tasks, get_task, add_task, complete_task, delete_task } from './schema.graphql'; // ❷

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache()
});
setClient(client); // ❸

// list
const listTasks = async () => {
  const reply = await query(list_tasks);
  reply.subscribe(data => console.log('list', data)); // ❹
};
listTasks();

// getTask
const getTask = async (tid) => {
  const reply = await query(get_task, { variables: { id: tid } });
  reply.subscribe(data => console.log('get', data));
};
getTask(3);

// addTask
const add = mutation(add_task);
const addTask = async (tname) => {
  const reply = await add({ variables: { name: tname } }); // ❺
  console.log('add', reply) 
};
addTask('New task');

// deleteTask
const del = mutation(delete_task);
const deleteTask = async (tid) => {
  const reply = await del({ variables: { id: tid } });
  console.log('del', reply) 
};
deleteTask(5);

// completeTask
const done = mutation(complete_task);
const completeTask = async (tid) => {
  const reply = await done({ variables: { id: tid } });
  console.log('done', reply) // ❻
};
completeTask(10);

Apolloクライアントをそのまま使う場合からの変更点:

  1. Apolloクライアントのgqlモジュールではなく、@rollup/plugin-graphqlでGraphQLを解釈する(いろんな実装があるのな)
  2. QueryとMutaionをschema.graphqlから読み込む。中身は以下参照。
  3. svelte-apolloをインポートして、デフォルトのクライアント・インスタンスをセット
  4. query()の返り値はSvelteのStoresをPromiseで包んだものなので、中身を除く場合はsubscribe()を使う。Storesが何をしているかはここを見ればざっくりわかる。一言で言うと複数コンポーネントの状態管理を楽にしてくれるオブジェクト。
  5. Mutaionの返り値はStoresではない
  6. 本題ではないが、console.log('label', reply)としてラベルづけするとわかりやすい
schema.graphql
query list_tasks {
  tasks {
    id
    name
    isActive
    createdAt
    updatedAt
    owner
  }
}

query get_task($id: ID!) {
  task(id: $id) {
    id
    name
  }
}

mutation add_task($name: String!) {
  addTask(name: $name) {
    id
    name
  }
}

mutation complete_task($id: ID!) {
  completeTask(id: $id) {
    id
    name
    isActive
  }
}

mutation delete_task($id: ID!) {
  deleteTask(id: $id) {
    id
    name
  }
}

replyはこんな感じ

list {loading: false, data: {}, error: undefined}
  data:
    tasks: Array(4)
      0: {__typename: "Task", id: "1", name: "Soak in an Onsen", isActive: true, createdAt: null,}
      1: {__typename: "Task", id: "2", name: "Sing Karaoke", isActive: false, createdAt: null,}
      2: {__typename: "Task", id: "3", name: "See cherry blossom", isActive: true, createdAt: null,}
      3: {__typename: "Task", id: "4", name: "Buy some milk", isActive: true, createdAt: 1621812683,}
      length: 4
      __proto__: Array(0)
    __proto__: Object
  error: undefined
  loading: false
  __proto__: Object

上記は、オブジェクトの中身を覗くためにconsole.log()してますが、実際には以下のように扱うのがSvelte流のようです。すっきりかけていい感じ。

const reply = query(list_tasks);

{#if $reply.loading}
  Loading...
{:else if $reply.error}
  Error: {$reply.error.message}
{:else}
  {#each $reply.data.tasks as task}
    <p>{task.id} {task.name}</p>
  {/each}
{/if}

GraphQLでのCRUDは出来るようになりました。ここまでくれば、あとはApp.svelteのHTML側をいじるだけです。

Rollupに怒られた件

初心者なので、Rollup先生からはいくつかお叱りをいただきました。

Error#1

bundle.js:5019 Uncaught ReferenceError: process is not defined
    at new ApolloClient (bundle.js:5019)

これは環境変数がうまく渡せてない。以下の@rollup/plugin-replaceで解決

https://github.com/rollup/plugins/tree/master/packages/replace

rollup.config.js
    replace({
      'process.env.NODE_ENV': JSON.stringify( 'development' )
    }),

Error#2

Error: Unexpected token (Note that you need plugins to import files that are not JavaScript)

GraphQLが解釈できていない。@rollup/plugin-graphqlで解決

https://github.com/rollup/plugins/tree/master/packages/graphql

Error#3

これはRollupじゃなくてApolloのエラー。cacheは必須。

Uncaught Invariant Violation: To initialize Apollo Client, you must specify a 'cache' property in the options object. 

Error#4

これはなんのエラーだったか?実行上関係なかった気がする。

Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for ca

次回

もうちょっと勉強のために、以前RESTで書いたTODOアプリをGraphQLに置き換えてみよう

シリーズ

脚注
  1. Readmeも微妙に間違ってる部分がある気がする。Apolloのバージョンが上がったからかもしれないが、そのままでは動かないところがあった。 ↩︎

Discussion