はじめに
今回のサンプルは以下に用意しました。
Apollo Server & Apollo Clientサンプル
以前こんな記事を書きました。
React(+Redux)+gRPCで実現するクリーンアーキテクチャ+マイクロサービス構成
BFFのフロントエンドAPI部分に関して、次の記事を見てクライアント通信の部分をGraphQLで実装できるとより柔軟で堅牢な気がしたので試してみました。
世のフロントエンドエンジニアにApollo Clientを布教したい
REST API、 GraphQLの違いに関しては下記を参考にしてください
アプリ開発の流れを変える「GraphQL」はRESTとどう違うのか比較してみた
GraphQLを導入するメリットはざっくり以下の通りです。
- APIのインタフェース定義(送信パラメータのデータ型定義)ができる(それにともない、データの型定義によるAPIパラメータのデータ型バリデーションチェックができる)
- データ取得条件に合わせてGETエンドポイントを大量に作成しなくて済む(GraphQLスキーマ単位になる)
- APIのバージョン管理ができる
- APIレスポンスのキャッシュが容易
反面デメリットとしては以下があります。
- 日本語の記事が少ない
- GraphQLインタフェース定義を学習するコストがかかる
Apollo Clientに関しては通信後のデータ管理をLinkという機能で保持する仕組みも持っているため、Reduxの代替としても期待されています。
ApolloでのGraphQL導入
ApolloはGraphQLのフロントエンド&バックエンドのライブラリです。
バックエンド側はApollo Server、フロントエンド側は Apollo Clientを導入する必要があります。
またGraphiQLというVisual Editorがツールとして付属しているのでAPIの動作確認を簡単に行うことができます。
バックエンド(Apollo Server)
各種NodeJSフレームワークに対応しています。
- express
- koa
- hapi
- restify
- lambda
- micro
- azure-functions
- adonis
今回はexpressで実装するのでexpress対応のapolloをnpmインストールします。
npm install --save apollo-server-express graphql-tools graphql express body-parser
次の実装が最小サンプルとなります。
const express = require('express')
const bodyParser = require('body-parser')
const { graphqlExpress, graphiqlExpress } = require('apollo-server-express')
const { makeExecutableSchema } = require('graphql-tools')
const app = express()
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())
// GraphQLスキーマ定義
const typeDefs = `
"""
type Query (必須)
"""
type Query { books: [Book] }
"""
返却するデータ構造
"""
type Book { title: String, author: String }
`
// ダミーデータ
const books = [
{
title: 'Harry Potter and the Sorcerer\'s stone',
author: 'J.K. Rowling',
},
{
title: 'Jurassic Park',
author: 'Michael Crichton',
},
]
// resolvers
const resolvers = {
Query: { books: () => books },
}
// スキーマ生成
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
// GraphQLエンドポイント
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema }))
// GraphiQL:GraphQLクエリのvisual editor
// TODO: 本番デプロイ時はアクセス出来ないようにする
app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' }))
app.listen(5000, () => {
console.log('Access to http://localhost:5000')
})
次コマンドで実行
$ node server.js
http://localhost:5000/graphiql
にアクセスすると
GraphiQLが表示されます。
次のようにクエリを書いて▶ボタンで実行するとダミーデータが取得できます。
query {
books {
title,
author,
}
}
GraphQLのスキーマ定義
細かい詳細は下記記事がまとまっています。
GraphQL入門 - 使いたくなるGraphQL
データの取得にはQuery、データの更新にはMutationを使います。
スキーマ定義にtype Query
もしくはtype Mutation
の定義は必須です。
スキーマ定義は次のように行います。
const typeDefs = `
type Query {
フィールド名(引数): 返却データ型
}
type Mutation {
フィールド名(引数): 返却データ型
}
}
`
// resolvers
const resolvers = {
Query: {
フィールド名: (引数) => 返却データ,
},
Mutation: {
フィールド名: (引数) => 返却データ,
},
}
データの基本型は次のようになっています。
- Int: 32bit整数型
- Float: 浮動小数型
- String: UTF-8 文字列型
- Boolean: true もしくは false
- ID: ユニークなスカラー値、キャッシュに使われる。Stringをシリアライズ(直列化)したデータで保存されている
加えて任意のオブジェクト単位にデータ型を定義できます。
次の例はBook型を定義した例です。
なお、パラメータを必須にしたい場合はデータ型末尾に!
をつけます。
type Query {
books: [Book]!,
}
type Book { title: String, author: String }
さらに詳細はGraphQL公式:Schemas and Typesを参考にしてください。
複数メソッドを定義するときはtype Query
もしくはtype Mutation
のブロック内にメソッドを追加します。
ちなみに"""
で囲めばコメントになります。
const typeDefs = `
type Query {
books: [Book],
items: [Item],
}
"""
返却するデータ構造
"""
type Book { title: String, author: String }
type Item { title: String }
`
// resolvers
const resolvers = {
Query: {
books: () => books,
items: () => items,
},
}
引数を取る場合はつぎのように書きます。
Resolver function signature
argsにパラメータが渡ってきます。
// GraphQLスキーマ定義
const typeDefs = `
type Query {
books(author: String): [Book],
}
type Book { title: String, author: String }
`
// resolvers
const resolvers = {
Query: {
books: (obj, args, context, info) => {
const author = args.author
return books.filter(book => book.author === author)
},
},
}
GraphiQLにて次のようなクエリを作成します。
query ($author: String!){
books(author: $author) {
title,
author,
}
}
QUERY VARIABLESには次の検索条件パラメータを指定
JSONのキーが$authorに入ります。
{
"author": "Michael Crichton"
}
フロントエンド(Apollo Client)
React + Apollo ClientからApollo Serverに通信を試みます。
まず、Apollo Clientをダウンロードします。
npm install --save apollo-boost react-apollo graphql-tag graphql
ApolloClientを生成して、ApolloProvider経由でアプリケーションコンポーネント<App />
をwrapすることで<App />
以下でgraphQLが使えるようになります。
/*globals module: false */
import React from 'react'
import ReactDOM from 'react-dom'
import { hot } from 'react-hot-loader'
import { ApolloProvider } from 'react-apollo'
import ApolloClient from 'apollo-boost'
import App from './App'
const client = new ApolloClient({
uri: 'http://localhost:5050/graphql',
})
const render = () => {
ReactDOM.render(
<ApolloProvider client={client}>
<App />
</ApolloProvider>
,
document.getElementById('root'),
)
}
// Webpack Hot Module Replacement API
hot(module)(render)
render()
App.jsです。qqlにクエリを記述します。
Queryコンポーネントで実際のAPIコールを行います。
API結果は{loading, error, data}
は返却されます。
使いやすいようにgraphQLというHOCを作成してwrapしてみました。
import React from 'react'
import gql from 'graphql-tag'
import { Query } from 'react-apollo'
// GraphQLクエリ
const query = gql`
query ($author: String!){
books(author: $author) {
title,
author,
}
}
`
// HOC
const graphQL = (param) => (WrappedComponent) => (props) => (
<Query
query={query}
variables={param}>
{({loading, error, data}) => <WrappedComponent {...props} loading={loading} error={error} data={data} />}
</Query>
)
class App extends React.Component {
render () {
const { loading, error, data } = this.props
if (loading) return <p>Loading...</p>
if (error) return <p>Error...</p>
return (<div>
{data.books.map(book =>
<div key={book.title}>
<h4>{book.title}</h4>
<span>{book.author}</span>
</div>
)}
</div>)
}
}
export default graphQL({
author: 'Michael Crichton',
})(App)
実行するとLoading表示からAPI取得後、データが表示されます。
Reduxのreducerとreact-reduxのconnectの置き換えみたいなことができました。
Apollo ClientでReduxの代用ができるのか
参考記事内でコラム: GraphQLはReduxを置き換えるのかとあったのですが、
Apollo ClientはGraphQLでの通信結果の状態管理はSSRなどを含めて通信周り(redux-thunkやredux-saga)の結果をReduxに入れてる箇所に関しては置き換えできそうでした。(主にreact-reduxのconnectとreducer周り)
逆に通信以外のアプリケーションデータに関して(例えばreact-router-reduxのような画面遷移状態をReduxに保持する等)を管理する機構が貧弱な(というか存在しない?)気がしました。(apollo-link-stateは通信周りだけっぽいですし・・・)
上記の事から、現状だとReduxを捨てることはできず、ローカルのStoreが2つ必要になるのでアンチパターンな気がしました。(逆にそこさえ解決できれば積極的に使いたい感)
Routingを見るとpropsにhistory渡せているっぽい?
GraphQLの設計思想自体はとても良いものなので通信周りはRest APIよりもスマートになると思いました。