華麗なるGatsby.jsの実践(型を変更してみよう)


GraphQLスキーマをカスタマイズする

先日、markdownのimgをgraphQLで管理しようとしたところ、型の類推エラーによってgraphQLのスキーマをいじらなければならなくなりました。なのでこのガイドを学習します!
ハイパー意訳がほぼほぼですのでご了承ください🙇‍♂️

graphQLのスキーマ言語をおおよそ知る必要がある

その前に、そもそも、graphQLに関する記述法を知らなかったので、そこをざっくり学習します。
https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000
私は上記で学習させていただきました!

特に必要となる知識は、スキーマ言語(Web APIの仕様を定義する方)の
fieldの定義方法と,Interfaceについてです。

type Query {
 currentUser: User!
}
interface Node {
 id: ID!
}

Interfaceは必要となるfieldのセットを束ねた抽象型です。

公式ガイド

では公式ガイドを見てみます。

このガイドは

  • プラグインの作成者

  • 自動型類推によって作成されたスキーマを修正したい方(私)

  • ビルドの最適化を測る方

  • スキーマに興味のある方

を対象としているようです。こちらかなり長いのですが、ここでは前編部分のみを扱います。
気になる方は続きを読むと良さそうです!

graphQLの強みは何と言っても多くの情報源を一括で扱えること!
そのためにはGraphQLスキーマを生成しなければなりません。

例 マークダウンとJSON

Markdownファイルからブログ記事を生成する。記事内容に加えて筆者情報をJSON形式で提供する。
時々ある寄稿者の情報は別のJSONとして保存する。

.md
---
title: Sample Post
publishedAt: 2019-04-01
author: [email protected]
tags:
  - wow
---
# Heading
Text
author.json
[
  {
    "name": "Doe",
    "firstName": "Jane",
    "email": "[email protected]",
    "joinedAt": "2018-01-01"
  }
]
contributor.json
[
  {
    "name": "Doe",
    "firstName": "Zoe",
    "email": "[email protected]",
    "receivedSwag": true
  }
]

これらをGraphQLでクエリするためにはgatsby-source-filesystem,gatsby-transformer-remark,gatsby-transformer-jsonを使用しますが、これらのプラグイン内部で何が行われているかというと、
markdownファイルをユニークなidとMarkdownRemarkタイプを持つnodeオブジェクトに変換しています。

同様に著者データはAuthorJsonタイプのnodeオブジェクトに変換され、
寄稿者データはContributorJson型のノードオブジェクトに変換されています。

Node interface

Source pluginやtransformer pluginによって作られるGatsbyのGraphQLのスキーマです。
Id, parent,children,またinternal型などといったfieldがセットされています。

interface Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
}

type Internal {
  type: String!
}

** TODO: internalって何でしょう..イマイチ調べてもわからずでした。

例 autor.json

例)Gatsby-transformer-jsonで作成するautor.json用のnode型は、以下のようになっています。

type AuthorJson implements Node {
  id: ID!
  parent: Node!
  children: [Node!]!
  internal: Internal!
  name: String
  firstName: String
  email: String
  joinedAt: Date
}

実際にgatsbyが作成したスキーマを確認するには、GraphQL Playgroundがオススメです。

プロジェクト上で

$ GATSBY_GRAPHQL_IDE=playground gatsby develop

そしてhttp://localhost:8000/___graphqlを開いて右画面側にある Schemeタブを開きます。

思った以上にぎっしりしてます。

自動型推論

先ほどのautor.jsonを見てみましょう。

[
  {
    "name": "Doe",
    "firstName": "Jane",
    "email": "[email protected]",
    "joinedAt": "2018-01-01"
  }
]

どこにも型定義などされていないですが、GraphQLではうまいこと動かせています。
これは型の類推を行なってくれているからですね。
1つ1つのfield内容を確認し、型をチェックすることで実現しています。

しかしこの型類推には2つの問題点があります。

  • 時間がかかり、負担がかかる

  • 型とデータが異なっていた場合、型の類推が失敗する

2つ目についてですが、例をあげます。

.md
  +  {
  +    "name": "Doe",
  +    "firstName": "John",
  +    "email": "[email protected]",
  +    "joinedAt": "201-04-02"
  +  }
  ]

joinedAt部分がスペルミスによって、Dateと解釈できなくなっています。

これらを解決するためには、型を明示的に示すことです。

## 型定義

createType actionを使って型を明示的に定義します。

gatsby-node.js
  exports.createSchemaCustomization = ({ actions }) => {
    const { createTypes } = actions
    const typeDefs = `
      type AuthorJson implements Node {
        joinedAt: Date
      }
    `
    createTypes(typeDefs)
  }

全ての型を定義する必要がない点に注意してください。(name,firstName等)

そもそも型推論をやらない!というストイックな方もいるでしょう。
これを行うことでパフォーマンスの向上が見込めます。先ほども開設したように、型の類推は負担が大きいため、大規模なプロジェクトほどパフォーマンス低下が著しくなります。
@dontInferディレクティブを用いて型推論をオプトアウトできます。

ディレクティブってなんだ?という方は先ほどのGraphQLスキーマについての解説をどうぞ!https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000)

gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    type AuthorJson implements Node @dontInfer {
      name: String!
      firstName: String!
      email: String!
      joinedAt: Date
    }
  `
  createTypes(typeDefs)
}

これがストイックなバージョンになります。
Node interfaceが提供するフィールドについては、Gatsby側で追加してくれるので明示しなくて大丈夫です。(id,parentなど)

ネストしている型

今まではString型やDate型などのスカラー型のみを扱ってきました。GraphQLでは他にも

  • ID型

  • Int型

  • Float型

  • Boolean型

  • JSON型

なども扱うことが可能です。また、複雑なオブジェクト値だって扱えます。

markdown-remarkを例にとってみます。

以下の記述によってfrontmatter.tagsは必ず文字の配列となります。

gatsby-node.js
  exports.createSchemaCustomization = ({ actions }) => {
    const { createTypes } = actions
    const typeDefs = `
      type MarkdownRemark implements Node {
        frontmatter: Frontmatter
      }
      type Frontmatter {
        tags: [String!]!
      }
    `
    createTypes(typeDefs)
  }

こちらの記述のように、直接Frontmatter型を指定すると失敗してしまいます。

gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = `
    # これは失敗!!!
    type Frontmatter {
      tags: [String]!
    }
  `
  createTypes(typeDefs)
}

なぜなら、Frontmatter型がソースプラグインやトランフォーマープラグインによって作成されないため、Nodeインターフェースが実装されくなってしまうからです。

そのためにこのような処理となります。