はじめに
この記事は GraphQL Advent Calendar 2019 の 23 日目の記事です。(空いていたのであとから埋めました) 今回はGo/React+Typescript(hooksベース)でGraphqlスタックの管理画面を作った時の技術選定と工夫した点などを書けたらと思います。
技術選定
基本「なるべく自動生成に頼る」方針で選定しました。
サーバー(Go)
- gqlgen: schema.graphqlからGraphQLサーバー生成くん
- dataloaden: Dataloaderコード生成くん
フロント(CRA + Typescript)
- gql-generator: schema.graphql から 全オペレーション xxx.gql ファイル生成君
- graphql-codegen: オペレーション ファイルから type生成君(typescript-react-apolloのwithHooks: true) 複数の schemaファイルから生成するにはこれしかなかった
開発サイクル
- schema.graphql 書く
- サーバ
gqlgen
でサーバー、 I/F, Model生成する(dataloaderが必要な場合はdataloaden
も使う)- resolver実装する
- フロント
gql-generator
でGraphQLオペレーション(*.gql
)生成する- ↑のコードを手で修正(フロントの要件に応じてネストする深さなど調節)
graphql-codegen
でTypescript/Reat-Apolloコード生成する
サーバー
gqlgen
schema.graphqlからGoサーバーに必要なコードを自動生成してくれるライブラリです。
まずは以下のような設定ファイルを書きます。
# gqlgen.yaml
schema:
- ../schema.graphql
exec:
filename: internal_gen.go
model:
filename: model/model_gen.go
resolver:
filename: resolver.go
type: Resolver
autobind: []
models:
# 自分で定義したscalarのモデルを参照する
Date:
model: github.com/igtm/xxx/server/model.Date
Month:
model: github.com/igtm/xxx/server/model.Month
# 指定fieldのリゾルバ化設定 (lazyにさせる)
User:
fields:
car: # User.car は1発で取りに行くと重いから別関数で取りに行くで
resolver: true
ポイントは models
です。カスタムのScalar
のGo実装への参照指定や、リゾルバの分割設定などを書いていきます。
自動生成されるファイルを区別するために xxx_gen.go
などといった命名規則にしておくといいかと思います。
dataloaden
gqlgenが勧めているDataloaderライブラリです。
上記で生成したmodelのdataloaderを作っていきます。
# dataloader生成 (名前は任意)
go run github.com/vektah/dataloaden $(NAME)$(TYPE)Loader $(TYPE) *github.com/igtm/xxx/server/model.$(NAME)
GoにはGenericsがないので、引数の型ごとに生成する必要があります。
使い方としては、middleware等でcontextにWithValueして埋め込んで適宜resolverから取り出して使う形になります。
懸念点: resolver.goだけは1度きりしか自動生成できない
resolver.goは実装を書いていくファイルなので、自動更新することができません。そのため手で関数などを作る必要があります。
懸念点: dataloadenは時間単位のbatchしかできないのが辛い
本家FacebookのNode.js実装Dataloaderは、batchしたい処理を同じTickに乗せることで過不足なく処理をまとめることができています。しかし、Goは仕組み上そのような実装方法を取ることができず、指定時間待つような微妙な実装になっています。そのため、指定時間が短すぎると、batchが実行されるのが早すぎて細切れで実行されたり、逆に長いと、無駄に時間がかかるようになります。
今回は 10ms
で設定しました。
フロントについて
gql-generator + graphql-codegen を使用
schema.graphqlから下記コマンドで *.gql を生成します。package.jsonの scriptsなどに書いておくといいですね。
gqlg --schemaFilePath ../schema.graphql --destDirPath ./src/gql --depthLimit 5
生成されるのは、fieldが全部もりもりの *.gql なので、クライアントの要件に応じて手で適時修正します。
schema.graphqlと *.gql から Typescript(React/Hooks) のコードを生成します。
overwrite: true
schema:
- ../schema.graphql
documents:
- "src/gql/**/*.gql"
generates:
src/gql/graphql-client-api.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
- "fragment-matcher"
config:
withComponent: false
withHooks: true
withHOC: false
reactApolloVersion: 3
生成されたtypescriptを使った使用例はこんな感じ
import { useUserQuery } from './gql/graphql-client-api'
const userQ = useUserQuery({
variables: {
userID: "hoge",
}
})
懸念点: *.gqlファイルを手で毎度修正するのが面倒
graphql-codegen で .tsファイルを生成するには
schema.graphql
- *.gql
の2種類が必要になります。そのため、*.gql ファイルを手で用意して上げる必要があるのですが、毎度schemaの変更毎にこちらのファイルも変更するのは面倒です。
gql-generator を使って *gqlを自動生成していますが、フロントの要件毎に微妙に必要な情報が異なるので、手で修正する必要があります。
しかし、fieldを追加したときなど、手で修正した内容が消えてしまうのは辛いです。そこで、いいかんじに差分をチェックしてマージしてくれるshellスクリプトを作りました。
#!/bin/sh | |
set -e | |
PJROOT_DIR=$(pwd) | |
CONFLICT_IDENTIFIER="=======" | |
# expected directory structure | |
# src/gql/queries/*.gql | |
# src/gql/mutations/*.gql | |
# ./schema.graphql | |
# make *.gql conflict like 'git merge' | |
conflictdiff () { | |
diff --unchanged-group-format="%=" --old-group-format="" --new-group-format="%>" --changed-group-format="<<<<<<< previous%c'\\12'%<=======%c'\\12'%>>>>>>>> new%c'\\12'" -wb $1 $2 | |
} | |
export -f conflictdiff | |
# already conflict | |
if grep -qr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql | |
then | |
echo -e "\033[31m[graphql] .gql files are conflicted. Firstly, fix below files by hand, and then try again. \033[m" | |
echo "conflicting files ..." | |
grep -nr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql | |
exit 1 | |
fi | |
# stash | |
rm -rf $PJROOT_DIR/src/gql_tmp | |
mv -f $PJROOT_DIR/src/gql $PJROOT_DIR/src/gql_tmp | |
# generate *.gql from schema.graphql using 'gql-generator' | |
gqlg --schemaFilePath ../schema.graphql --destDirPath ./src/gql --depthLimit 5 | |
pushd $PJROOT_DIR/src > /dev/null | |
ops=( | |
"queries" | |
"mutations" | |
) | |
for op in "${ops[@]}" ; do | |
rm -rf gql_tmp2 | |
mkdir -p gql_tmp2/$op | |
# make them conflict | |
( ls -l gql/$op ; ls -l gql_tmp/$op ) | awk '{print $9}' | sort | uniq -d | grep .gql | xargs -I{} sh -c "conflictdiff gql_tmp/${op}/{} gql/${op}/{} > gql_tmp2/${op}/{}; cat gql_tmp2/${op}/{} > gql/${op}/{}" | |
rm -rf gql_tmp2 | |
done | |
popd > /dev/null | |
rm -rf $PJROOT_DIR/src/gql_tmp | |
# print if *.gql conflicts | |
if grep -qr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql | |
then | |
echo -e "\033[31m[graphql] .gql files are conflicted. fix below files by hand. \033[m" | |
echo "conflicting files ..." | |
grep -nr $CONFLICT_IDENTIFIER $PJROOT_DIR/src/gql | |
exit 0 | |
fi |
出力形式は、git mergeでコンフリクトした時にできるアレ(>>>>>>>>)です。VSCodeだと簡単に修正できるので、以下のようにポチポチ押すだけで楽なので、その書式に沿うようにしてます。
まとめ
今回は gqlgen/graphql-codegenを使うことでschemaファイルからほとんど自動でコード生成することができました。
また、 *gqlファイルもスクリプト使えばいい感じに更新できるようになりました。
この環境でどんどん開発していきたいなと思ってます。
またgqlgenでロール権限管理した話もどこかでしたいと思ってます。