kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Material-UI の GridList コンポーネントを実装する

前回の記事から少し時間がたってしまったけど,Material-UI を使ったプロトタイプ開発を続けている.今回は GridList コンポーネントをサンプルコードを参考に実装しながら理解を深めていく.グリッドリストはフォトリストのようにコンテンツを並べる UI のことを言う.過去には List コンポーネントと Snackbars コンポーネントの記事を書いていて,コンポーネントの調査シリーズも定期的に書いていく.

material-ui.com

なお,実装したサンプルコードは GitHub に公開してある.TypeScript で create-react-app を実行してから実装を進めた.記事に載せるコードはポイントを限定し抜粋するため,実際にコード全体を見る場合は GitHub を参照して頂ければと!

$ create-react-app sandbox-material-ui-grid-list --template typescript
$ cd sandbox-material-ui-grid-list

$ npm install @material-ui/core
$ npm install @material-ui/icons

$ yarn start

github.com

今までは create-react-app --typescript を使っていたけど,最新版の v3.3.0 から以下の警告が出るようになっていた.--typescript オプションは廃止になり,今後は create-react-app --template typescript を使う必要がある.覚えておこう.

The --typescript option has been deprecated and will be removed in a future release.
In future, please use --template typescript.

GridList コンポーネント

GridList コンポーネントはグリッドリストの「全体枠」を定義する.

主要なパラメータは2個ある.まず,cellHeight プロパティを使うと,グリッドリストの「高さ」を設定できる.ピクセル固定もできるし,自動なら auto も設定できる.次に,cols プロパティを使うと,グリッドリストの「タイル数(横)」を設定できる.GridList コンポーネントで使えるプロパティ一覧は以下のドキュメントに載っている.

material-ui.com

サンプルコードの一部を載せておく.

const App: React.FC = () => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <GridList cellHeight={200} className={classes.gridList} cols={3}>
      </GridList>
    </div>
  );
}

GridListTile コンポーネント

GridListTile コンポーネントはグリッドリストの「タイル」を定義する.

GridList コンポーネントと GridListTile コンポーネントを組み合わせることにより,「全体枠」の中に「タイル」を配置できる.GridList 側で設定した cols に対して,GridListTile 側でさらに cols を設定できる.cols="1" にすれば「等間隔にタイルを敷き詰める」ことができるし,cols="1"cols="2" を組み合わせれば「特定のタイルのサイズを変える」こともできる.GridListTile コンポーネントで使えるプロパティ一覧は以下のドキュメントに載っている.

material-ui.com

サンプルコードの一部を載せておく.

const App: React.FC = () => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <GridList cellHeight={200} className={classes.gridList} cols={3}>
        <GridListTile key="cat" cols="2">
          <img src={cat} alt="cat" />
        </GridListTile>
        <GridListTile key="deer" cols="1">
          <img src={deer} alt="deer" />
        </GridListTile>
        <GridListTile key="kingfisher" cols="1">
          <img src={kingfisher} alt="kingfisher" />
        </GridListTile>
        <GridListTile key="koala" cols="1">
          <img src={koala} alt="koala" />
        </GridListTile>
        <GridListTile key="pelikan" cols="1">
          <img src={pelikan} alt="pelikan" />
        </GridListTile>
        <GridListTile key="rabbit" cols="1">
          <img src={rabbit} alt="rabbit" />
        </GridListTile>
        <GridListTile key="tiger" cols="2">
          <img src={tiger} alt="tiger" />
        </GridListTile>
      </GridList>
    </div>
  );
}

実際に動作確認をすると,cols="2" に設定したネコとトラは2タイルを結合したサイズになっている.なお,動物の素材は Pixabay から取得している.

f:id:kakku22:20200112123227p:plain

GridListTileBar コンポーネント

GridListTileBar コンポーネントはタイルの上に表示する「追加情報」を定義する.

主要なパラメータは3個ある.まず,titlesubtitle を使うと追加情報をタイルの上に表示できる.さらに,actionIcon を使うと「ボタンを押したら○○」というトリガーの実装と連携することもできる.GridListTileBar コンポーネントで使えるプロパティ一覧は以下のドキュメントに載っている.

material-ui.com

サンプルコードの一部を載せておく.titletitle + subtitletitle + subtitle + actionIcon の計3パターンを実装した.

const App: React.FC = () => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <GridList cellHeight={200} className={classes.gridList} cols={3}>
        <GridListTile key="cat" cols="2">
          <img src={cat} alt="cat" />
          <GridListTileBar
            title="Cat"
          />
        </GridListTile>

        {/* 中略 */}

        <GridListTile key="koala" cols="1">
          <img src={koala} alt="koala" />
          <GridListTileBar
            title="Koala"
            subtitle="So Cute !"
          />
        </GridListTile>

        {/* 中略 */}

        <GridListTile key="tiger" cols="2">
          <img src={tiger} alt="tiger" />
          <GridListTileBar
            title="Tiger"
            subtitle="So Cool !"
            actionIcon={
              <IconButton className={classes.icon}>
                <InfoIcon />
              </IconButton>
            }
          />
        </GridListTile>
      </GridList>
    </div>
  );
}

実際に動作確認をすると,ネコとコアラとトラに追加情報が表示されている.

f:id:kakku22:20200112123308p:plain

ListSubheader コンポーネント

ListSubheader コンポーネントはグリッドリストの中に「区切り」を定義する.

GridListTile コンポーネントの中に ListSubheader コンポーネントを含めるため,サイズなどは GridListTile コンポーネントの cols プロパティを使う.ListSubheader コンポーネントで使えるプロパティ一覧は以下のドキュメントに載っている.

material-ui.com

サンプルコードの一部を載せておく.写真を撮影した年を区切りとして追加した.

const App: React.FC = () => {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <GridList cellHeight={200} className={classes.gridList} cols={3}>
        <GridListTile key="Subheader" cols={3} style={{ height: 'auto' }}>
          <ListSubheader component="div">2018</ListSubheader>
        </GridListTile>
        <GridListTile key="cat" cols="2">
          <img src={cat} alt="cat" />
          <GridListTileBar
            title="Cat"
          />
        </GridListTile>
        <GridListTile key="deer" cols="1">
          <img src={deer} alt="deer" />
        </GridListTile>

        {/* 中略 */}

        <GridListTile key="Subheader" cols={3} style={{ height: 'auto' }}>
          <ListSubheader component="div">2019</ListSubheader>
        </GridListTile>
        <GridListTile key="rabbit" cols="1">
          <img src={rabbit} alt="rabbit" />
        </GridListTile>
        <GridListTile key="tiger" cols="2">
          <img src={tiger} alt="tiger" />
          <GridListTileBar
            title="Tiger"
            subtitle="So Cool !"
            actionIcon={
              <IconButton className={classes.icon}>
                <InfoIcon />
              </IconButton>
            }
          />
        </GridListTile>
      </GridList>
    </div>
  );
}

実際に動作確認をすると,2018年の区切り(ヘッダー)と2019年の区切り(ヘッダー)が表示されている.

f:id:kakku22:20200112123336p:plain

まとめ

Material-UIGridList コンポーネントを実装しながら理解を深めた.コード量を少なくグリッドリストの実装ができて便利だった.引き続きコンポーネント調査を続けていくぞ!

Material-UI 関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com

2019年(7-12月)のプルリクエストを振り返る

OSS に送ったプルリクエストを振り返ろうと思う.プルリクエストの振り返りは2016年からしているけど,2019年は前半に多く送った背景もあり,既に「2019年(1-6月)」の期間で記事を書いている.今回は後半として「2019年(7-12月)」を振り返ろうと思う.後半の累計は「計5件」となり,2019年全体だと「計17件」となる.過去の振り返りは以下にある.

プルリクエストを振り返るための検索

プルリクエストを振り返るために GitHub の検索条件を使う.今回は「2019年(7-12月)」に限定する必要があるため created:2019-07-01..2019-12-31 を使う.

is:pr is:public author:kakakakakku -user:kakakakakku created:2019
is:pr is:public author:kakakakakku -user:kakakakakku created:2019-07-01..2019-12-31

2019/11

awsdocs/aws-cloudformation-user-guide

ドキュメントを読みながら AWS CloudFormation テンプレートの写経をしていたら,プロパティ名に誤りを発見したため,修正した.

github.com

awslabs/aws-sam-cli

AWS SAM を検証していたら,CLI のエラーメッセージに誤ったオプションが記載されていたため,修正した.

github.com

2019/12

envoyproxy/katacoda-scenarios

11月から Envoy を学ぶため「Try Envoy」のコンテンツを活用している.Envoy のドキュメントにデッドリンクがあったり,名称が統一されていなかったり,進めていると気になる誤りがあったため,コツコツと修正している.Merge はされているものの,実際に Katacoda にデプロイされるタイミングはわからず,気長に待ちたいと思う.

github.com github.com github.com

まとめ

2019年(7-12月)は「計5件」のプルリクエストを送ることができた.日に日にコードを書く機会が減っていることもあり,プルリクエストを送る機会も減っているけど,来年もコツコツと頑張る!

Envoy の Health Checking と Outlier Detection の違いを学べる「Detecting Down Services with Health Checks」を試した

今回は「Try Envoy」「Detecting Down Services with Health Checks」を紹介する.高可用性のために Envoy でサポートされている「ヘルスチェック (Health Checking)」「外れ値検出 (Outlier Detection)」を学べる.

Detecting Down Services with Health Checks

手順は以下の「計8種類」ある.

  • Step.1 「Proxy Configuration」
  • Step.2 「Add Health Check」
  • Step.3 「Start Proxy」
  • Step.4 「Failed Services」
  • Step.5 「Healthy Services」
  • Step.6 「Total Failure」
  • Step.7 「Outlier Detection Configuration」
  • Step.8 「Testing Outlier Detection」

www.envoyproxy.io

www.katacoda.com

Step.1 「Proxy Configuration」

まず,用意された envoy.yaml の設定を確認する.もう完全に読み慣れたと思う.ポイントは clusters で,全てのリクエストを 172.18.0.3172.18.0.4 にラウンドロビンでルーティングする.例えば「もし一部のエンドポイントに障害が発生した場合」に Envoy ではどう対応したら良いのだろう?そこで「ヘルスチェック (Health Checking)」を使う.

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8080 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: backend
              domains:
                - "*"
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: targetCluster
          http_filters:
          - name: envoy.router
  clusters:
  - name: targetCluster
    connect_timeout: 0.25s
    type: STRICT_DNS
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    hosts: [
      { socket_address: { address: 172.18.0.3, port_value: 80 }},
      { socket_address: { address: 172.18.0.4, port_value: 80 }}
    ]

Step.2 「Add Health Check」

ヘルスチェックは clusters の中に設定する.代表的なパラメータは以下となる.

  • interval : 間隔
  • unhealthy_threshold : 異常と判断する閾値
  • healthy_threshold : 正常と判断する閾値
  • http_health_check.path : ヘルスチェックをする URL

今回は /health に対して「10秒間隔」でヘルスチェックをし,1回成功すると正常と判断する.他にも「Jitter(ランダムな遅延時間)」の設定も入っている.

clusters:
- name: targetCluster
  connect_timeout: 0.25s
  type: STRICT_DNS
  dns_lookup_family: V4_ONLY
  lb_policy: ROUND_ROBIN
  hosts: [
    { socket_address: { address: 172.18.0.3, port_value: 80 }},
    { socket_address: { address: 172.18.0.4, port_value: 80 }}
  ]
  health_checks:
    - timeout: 1s
      interval: 10s
      interval_jitter: 1s
      unhealthy_threshold: 6
      healthy_threshold: 1
      http_health_check:
        path: "/health"

他にも細かなパラメータは多くあるため,必要に応じてドキュメントを参照する.

www.envoyproxy.io

Step.3 「Start Proxy」

さっそく Envoy を起動する.同時にバックエンドのホストとして katacoda/docker-http-server:healthy を2個起動する.コンテナイメージ(healthy タグ)の解説は書かれてなく,あくまで予想だけど,デフォルトでヘルスチェックに成功する実装になっていると思う.そして curl で状態を変えることもできる.

$ docker run -d --name proxy1 -p 80:8080 -v /root/:/etc/envoy envoyproxy/envoy

$ docker run -d katacoda/docker-http-server:healthy

$ docker run -d katacoda/docker-http-server:healthy

Envoy にリクエストを送ると,正常に動いている.やっと検証環境が整った!

$ curl localhost
<h1>A healthy request was processed by host: daab80baf4ae</h1>

$ curl localhost
<h1>A healthy request was processed by host: 80461e934cfd</h1>

$ curl localhost
<h1>A healthy request was processed by host: daab80baf4ae</h1>

Step.3 完了時点で構成図は以下のようになる.

f:id:kakku22:20200107012133p:plain

Step.4 「Failed Services」

意図的に障害を発生させるため,172.18.0.3 に対して /unhealthy エンドポイントを呼び出す.すると,レスポンスに unhealthy request と表示される.レスポンスコードも 500 となり,1個のエンドポイントを落とすことができた.

$ curl 172.18.0.3/unhealthy

$ curl 172.18.0.3 -i
HTTP/1.1 500 Internal Server Error
(中略)
<h1>A unhealthy request was processed by host: daab80baf4ae</h1>

実際に while を使って 0.5 秒ごとに curl をすると,unhealthy_threshold に該当したタイミングから unhealthy request の発生は止まる.期待通りにヘルスチェックが動いていることを確認できる.

$ while true; do curl localhost; sleep .5; done
<h1>A unhealthy request was processed by host: daab80baf4ae</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A unhealthy request was processed by host: daab80baf4ae</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A unhealthy request was processed by host: daab80baf4ae</h1>

(中略)

<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>
<h1>A healthy request was processed by host: 80461e934cfd</h1>

Step.4 完了時点で構成図は以下のようになる.

f:id:kakku22:20200107012153p:plain

Step.5 「Healthy Services」

障害を復旧するため,172.18.0.3 に対して /healthy エンドポイントを呼び出す.すると,またエンドポイントは2個に戻る.

$ curl 172.18.0.3/healthy

$ curl localhost
<h1>A healthy request was processed by host: daab80baf4ae</h1>

$ curl localhost
<h1>A healthy request was processed by host: 80461e934cfd</h1>

$ curl localhost
<h1>A healthy request was processed by host: daab80baf4ae</h1>

Step.6 「Total Failure」

今度は全面障害を発生させるため,172.18.0.3172.18.0.4 に対して /unhealthy エンドポイントを呼び出す.

$ curl 172.18.0.3/unhealthy

$ curl 172.18.0.4/unhealthy;

「Try Envoy」の手順だと Envoy から 503 が返ってくると書いてあるけど,実際にはそうならなかった.全面障害になると,全てのエンドポイントにリクエストをルーティングしているため,期待していた挙動とも異なる.設定変更など,もう少し調査が必要そう.なんだろう.

$ curl localhost -i
HTTP/1.1 500 Internal Server Error
(中略)
<h1>A unhealthy request was processed by host: daab80baf4ae</h1>

$ curl localhost -i
HTTP/1.1 500 Internal Server Error
(中略)
<h1>A unhealthy request was processed by host: 80461e934cfd</h1>

Step.7 「Outlier Detection Configuration」

Step.7 と Step.8 は「外れ値検出 (Outlier Detection)」を試す.ドキュメントを読むと,以下のように表現されている.なお「ヘルスチェック」「外れ値検出」は併用もできる.

  • "Active" Health Checking : ヘルスチェック (Health Checking)
  • "Passive" Health Checking : 外れ値検出 (Outlier Detection)

ActivePassive という表現にもある通り,意図的にリクエストを投げて正常を確認するのが「ヘルスチェック」で,エンドポイントからのレスポンスをダイナミックに確認して正常を確認するのが「外れ値検出」となる.

www.envoyproxy.io

実際のレスポンスを判断材料にするため,設定項目は少なく使うことができる.envoy.yaml は以下のようになり,5xx のレスポンスコードが「3回」連続して返された場合に base_ejection_time に設定した時間はルーティング対象から除外する(Ejection と言う).その後もう1度ルーティング対象になる.

clusters:
- name: targetCluster
  connect_timeout: 0.25s
  type: STRICT_DNS
  dns_lookup_family: V4_ONLY
  lb_policy: ROUND_ROBIN
  hosts: [
    { socket_address: { address: 172.18.0.5, port_value: 80 }},
    { socket_address: { address: 172.18.0.6, port_value: 80 }}
  ]
  outlier_detection:
      consecutive_5xx: "3"
      base_ejection_time: "30s"

ただし,base_ejection_time は実際にはもう少し複雑で,ドキュメントを読むと以下のように書いてある.ようするにbase_ejection_time に設定した時間に Ejection された回数を掛ける」となり,繰り返し 5xx を返すエンドポイントほど「長時間 Ejection される」アプローチが実装されている.

The base time that a host is ejected for. The real time is equal to the base time multiplied by the number of times the host has been ejected.

www.envoyproxy.io

Step.8 「Testing Outlier Detection」

最後は「外れ値検出」を試す.katacoda/docker-http-server:healthy を追加で2個起動してから outlier_detection の設定をした Envoy も起動する.

$ docker run -d katacoda/docker-http-server:healthy

$ docker run -d katacoda/docker-http-server:healthy

$ docker run -d --name proxy2 -p 81:8080 \
    -v /root/:/etc/envoy \
    -v /root/envoy1.yaml:/etc/envoy/envoy.yaml \
    envoyproxy/envoy

while を使って 0.5 秒ごとに curl をしながら 172.18.0.5 に対して /unhealthy エンドポイントを呼び出すと,計3回 unhealthy が出て,その後は止まっている.期待通りに外れ値検出が動いていることを確認できる.

$ curl 172.18.0.5/unhealthy

$ while true; do curl localhost:81; sleep .5; done
<h1>A healthy request was processed by host: f586da250a29</h1>
<h1>A unhealthy request was processed by host: 932aa85e6053</h1>
<h1>A healthy request was processed by host: f586da250a29</h1>
<h1>A unhealthy request was processed by host: 932aa85e6053</h1>
<h1>A healthy request was processed by host: f586da250a29</h1>
<h1>A unhealthy request was processed by host: 932aa85e6053</h1>
<h1>A healthy request was processed by host: f586da250a29</h1>
<h1>A healthy request was processed by host: f586da250a29</h1>
<h1>A healthy request was processed by host: f586da250a29</h1>

まとめ

  • 「Try Envoy」のコンテンツ「Detecting Down Services with Health Checks」を試した
  • Envoy では「ヘルスチェック (Health Checking)」以外に「外れ値検出 (Outlier Detection)」もあることを学べた

引き続き,進めていくぞ!

プルリクエスト

試しながら気付いた誤りを修正してプルリクエストを送っておいた!

github.com

Try Envoy 関連

mdline を使ってタイムライン(年表)を作ろう!

「mdline」を使うと「タイムライン(年表)」を簡単に作れる.実装は必要なく,Markdown から HTML に変換できる.シチュエーションは限定的かもしれないけど,非常に面白く,試してみた!

github.com

Ruby Releases History

今回は mdline を試すサンプルとして「Ruby Releases History」を作った.Ruby のバージョンごとにリリース日をタイムラインとしてプロットしている.そして Netlify に配信をしたため,以下の URL から実際にタイムラインを見れるようにしてある.

f:id:kakku22:20200106203239p:plain

リリース日など,データセットは以下の公式サイトを参考にした.流石に量が多く,今回は Mechanize を使ってシュッと Markdown を作った.

www.ruby-lang.org

コード関連は全て GitHub に公開してある.Netlify のビルド設定はプロジェクト設定にすることもできるけど,今回は netlify.toml を使ってリポジトリ管理にしてみた.Netlify も機能が多くてたくさん遊べそう!

github.com

mdline フォーマット

GitHub に記載されている通り,フォーマットは非常にシンプルで,以下の2種類となる.

## {{Date}}: TITLE

MARKDOWN BODY

## {{Date}}--{{Date}}: TITLE

MARKDOWN BODY

今回作った「Ruby Releases History」の一部を抜粋すると以下のようになる.

## 2002-03-01: Ruby 1.6.7

[more...](https://www.ruby-lang.org/en/news/2002/03/01/167-is-released/)

## 2003-08-04: Ruby 1.8.0

[more...](https://www.ruby-lang.org/en/news/2003/08/04/ruby-180-released/)

mdline 実行

1番簡単に使うなら「mdline CLI」をインストールし,mdline コマンドを実行する.オプションは少なく,HTML を指定する -o--output 以外はなかった.例えば <title> を指定できるようにしたり,あると便利な機能はまだまだありそう.

$ npm install -g mdline
$ mdline ruby-releases.md -o ruby-releases.html

まとめ

「mdline」を使ってオリジナルな「タイムライン(年表)」を作ろう!

Apollo Server と Apollo Client を写経しながら GraphQL を学べる「初めての GraphQL」を読んだ

2019年11月に発売された「初めての GraphQL」を読んだ.1度ザッと読んだ後に,気になっていた Apollo ServerApollo Client の実装を写経しながら理解を深めていたため,書評をまとめるのに少し遅れてしまった.

タイトルに「初めての」とある通り,GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった.特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしく,一言で表現すると「知りたい!を知れる本」かなと!5章と6章は時間を取って写経するのが良いと思う.

目次

  • 1章 : GraphQLへようこそ
  • 2章 : グラフ理論
  • 3章 : GraphQLの問い合わせ言語
  • 4章 : スキーマの設計
  • 5章 : GraphQLサーバーの実装
  • 6章 : GraphQLクライアントの実装
  • 7章 : GraphQLの実戦投入にあたって
  • 付録A : Relay各仕様解説

以下のサイトに誤植は公開されていなかった.正直言って「初版第1刷」は誤植がそこそこある.記事の最後にメモ程度に残しておく.

www.oreilly.co.jp

GraphQL API とクエリ環境

本書を読みながら理解度を高めるため,気軽に試せる GraphQL API とクエリ環境を準備しておくと良いと思う.本書では「Snowtooth GraphQL API」をメインで使うけど,他にも「GitHub GraphQL API」「Star Wars API」もある.

また,個人的に好きな「GraphQL Pokémon」もあり,詳しくは前回の記事にまとめてある.

kakakakakku.hatenablog.com

クエリ環境は「GraphQL Playground (Web / App)」「GraphiQL (Web / App)」など,慣れたもので良いと思う.個人的には「GraphQL Playground」の Mac App を使っている.

GraphQL オペレーション

3章「GraphQLの問い合わせ言語」では,GraphQL オペレーション(querymutationsubscription)を学ぶ.本書で使う「Snowtooth GraphQL API」「スノートゥース山」という名前のゲレンデ(架空)のリフト情報とトレイル(コース)情報を管理する.

1. query

まず query オペレーションでは,シンプルなクエリを実行したり,複数クエリを実行したり,条件付きのクエリを実行したり,ステップバイステップにクエリを学べる.また「GraphQL Pokémon」の記事でも紹介した「フラグメント」も本書で紹介されている.以下のクエリは liftCount()allLifts()allTrails() の3種類のクエリを実行し,リフト件数は status: OPEN の条件付きとなる.

query liftsAndTrails {
  liftCount(status: OPEN)
  allLifts {
    name
    status
  }
  allTrails {
    name
    difficulty
  }
}

クエリを実行すると,以下のように結果が返ってくる.

f:id:kakku22:20200105051036p:plain

2. mutation

次に mutation オペレーションでは,データ更新を実行するミューテーションクエリを学べる.構文だけではなく,実際に「Snowtooth GraphQL API」を使って試すこともできる.公開 API だけど,実はリフトとトレイルのステータスを更新する setLiftStatus()setTrailStatus() は使えるようになっている.ビックリ!

f:id:kakku22:20200105051053p:plain

実際にリフトのステータスを CLOSED に更新するミューテーションクエリは以下となる.

mutation closeLift {
  setLiftStatus(id: "jazz-cat", status: CLOSED) {
    name
    status
  }
}

3. subscription

最後に subscription オペレーションでは,GraphQL のポイントとも言える「WebSocket を使ったデータのリアルタイム反映」を学べる.以下のようなサブスクリプションクエリを実行すると,データの更新待ちになるため,別途ミューテーションクエリを実行すると,すぐに反映される.REST のように定期的に API を実行する必要もなく,データによっては便利な機能だと思う.

subscription {
  liftStatusChange {
    name
    capacity
    status
  }
}

f:id:kakku22:20200105051110p:plain

なお,「GraphQL Playground」の Mac Appだと WebSocket をローカルホストに接続するため,うまく動かなかった.設定変更をすることもできず,今回は Web で確認した.

{
  "error": "Could not connect to websocket endpoint ws://localhost:4000/. Please check if the endpoint url is correct."
}

GraphQL スキーマ : 多対多

4章「スキーマの設計」では,写真共有アプリケーションをテーマとし,GraphQL スキーマの仕様と「スキーマファースト」と呼ばれる設計思想を学べる.そして,シンプルな Photo 型だけではなく,User 型と多対多の関係を作るために中間テーブルを用意したりする「よくある設計」に関してもまとまっていて良かった.

例えば,よくある「タグ付け」という機能を実装する場合,以下のように Photo 型と User 型に「タグ付け」を表現するフィールドを追加する.!「null ではない」を意味するため,[Photo!]!「null ではない配列に,null ではない Photo が入っている」となる.

type User {
  (中略)
  inPhotos: [Photo!]!
}

type Photo {
  (中略)
  taggedUsers: [User!]!
}

さらに,型同士に関係だけではなく,追加情報(例えば「知り合ってからの期間」)も持たせたい場合は,新しく型を作ることになる.これを「スルー型」と呼ぶ.以下のように Friendship 型を定義し,追加情報は Friendship 型に持たせられる.

type User {
  friendship: [Friendship!]!
}

type Friendship {
  friend_a: User!
  friend_b: User!
  howLong: Int!
  whereWeMet: Location
}

GraphQL サーバの実装を写経する : apollo-server

5章「GraphQLサーバーの実装」では,apollo-server を使って,実際に GraphQL サーバを実装していく.GraphQL のクエリを実行するためには,リゾルバ(特定のデータを返す関数)が必要となり,実際に実装するのは大変だと思う.

github.com

本書を流し読みするだけだと理解が浅くなりそうだったので,時間を取って写経をしてみた.是非写経をオススメするけど,今回はその一部を載せておこうと思う.なお,完成形は GitHub に公開されているので,動作確認から先に進めても良いと思う.

github.com

まず最初にプロジェクトを作成する.Apollo 関連とホットリロードをするための nodemon もインストールしておく.

$ npm init -y
$ npm install apollo-server graphql nodemon

さっそく index.js を作成する.構成としてはクエリを定義した typeDefs (型定義)resolvers (リゾルバ実装) となる.そして,最後に typeDefsresolvers を指定した Apollo Server を起動する.totalPhotos() を実行すると,固定値 42 を返す.

const {
    ApolloServer
} = require(`apollo-server`)

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }
`

const resolvers = {
    Query: {
        totalPhotos: () => 42
    }
}

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server
    .listen()
    .then(({
        url
    }) => console.log(`GraphQL Service running on ${url}`))

さっそく npm start で Apollo Server を起動する.

$ npm start
(中略)
GraphQL Service running on http://localhost:4000/

うまく起動できていると,GraphQL Playground でクエリを実行できる.以下のようになれば OK!

{
  totalPhotos
}

f:id:kakku22:20200105051336p:plain

次にミューテーションを実装する.名前は写真を登録するため postPhoto() とする. パラメータとしては namedescription を定義する.登録後は Boolean を返す定義となり,今回は固定値 true 返す.リゾルバとしては,配列 photos にデータを追加する実装になっている.コードの差分を中心に以下に載せた.

// 中略

const typeDefs = `
    type Query {
        totalPhotos: Int!
    }

    type Mutation {
        postPhoto(name: String! description: String): Boolean!
    }
`

var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length
    },
    Mutation: {
        postPhoto(parent, args) {
            photos.push(args)
            return true
        }
    }
}

// 中略

動作確認のために,まず Query Variables に以下の JSON を定義する.

{
  "name": "sample photo A",
  "description": "A sample photo for our dataset"
}

そして,ミューテーションクエリを実行する.

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description)
}

すると,リゾルバの実装通りに true が返ってくる.

{
  "data": {
    "postPhoto": true
  }
}

f:id:kakku22:20200105051355p:plain

実際に使おうとすると,写真の一覧が欲しかったり,ミューテーションクエリから true が返ってくるのは微妙だったりする.次に allPhotos() クエリを追加したり,ミューテーションクエリから追加した写真を返せるようにする.そのために Photo 型を定義したり,postPhoto() の定義で Photo を返すように修正したり,ID を連番で採番するように修正している.コードの差分を中心に以下に載せた.

// 中略

const typeDefs = `
    type Photo {
        id: ID!
        url: String!
        name: String!
        description: String
    }

    type Query {
        totalPhotos: Int!
        allPhotos: [Photo!]!
    }

    type Mutation {
        postPhoto(name: String! description: String): Photo!
    }
`

var _id = 0
var photos = []

const resolvers = {
    Query: {
        totalPhotos: () => photos.length,
        allPhotos: () => photos
    },
    Mutation: {
        postPhoto(parent, args) {
            var newPhoto = {
                id: _id++,
                ...args
            }
            photos.push(newPhoto)
            return newPhoto
        }
    },
    Photo: {
        url: parent => `http://yoursite.com/img/${parent.id}.jpg`
    }
}

// 中略

フィールドを指定したミューテーションクエリを実行する.

mutation newPhoto($name: String!, $description: String) {
  postPhoto(name: $name, description: $description) {
    id
    name
    description
  }
}

すると,ちゃんと Photo 型の結果が返ってきた.

{
  "data": {
    "postPhoto": {
      "id": "3",
      "name": "sample photo A",
      "description": "A sample photo for our dataset"
    }
  }
}

ミューテーションクエリを数回実行した後に追加した allPhotos() クエリを実行する.

query listPhotos {
  allPhotos {
    id
    name
    description
    url
  }
}

写真の一覧を取得できる.

{
  "data": {
    "allPhotos": [
      {
        "id": "0",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/0.jpg"
      },
      {
        "id": "1",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/1.jpg"
      },
      {
        "id": "2",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/2.jpg"
      },
      {
        "id": "3",
        "name": "sample photo A",
        "description": "A sample photo for our dataset",
        "url": "http://yoursite.com/img/3.jpg"
      }
    ]
  }
}

残りは以下の項目などを実装していくことになる.

  • enum 型 と input 型を使って使って型定義をモデル化する(デフォルトインプットを指定する)
  • Photo 型 と User 型を連携する

GraphQL サーバの実装を写経する : apollo-server-express

5章「GraphQLサーバーの実装」にはまだ続きがある.Apollo Server を既存のアプリケーションに追加したり,より細かな機能を Express ミドルウェアとして利用したり,様々な用途を考えて apollo-server-express を使ったリファクタリングをする.試す場合は,以下のように apollo-server-express などをインストールしておく.

$ npm remove apollo-server
$ npm install apollo-server-express express
$ npm install graphql-playground-middleware-express

Express を使う場合は,以下のような実装になる.ウェブページを表示したり,GraphQL Playground を表示したり,必要に応じてミドルウェアを追加できる.

const {
    ApolloServer
} = require(`apollo-server-express`)
const express = require(`express`)
const expressPlayground = require(`graphql-playground-middleware-express`).default

// 中略

var app = express()

const server = new ApolloServer({
    typeDefs,
    resolvers
})

server.applyMiddleware({
    app
})

app.get(`/`, (req, res) => res.end(`Welcome to the PhotoShare API`))
app.get(`/playground`, expressPlayground({
    endpoint: `/graphql`
}))

app
    .listen({
            port: 4000
        }, () => console.log(`GraphQL Service running on @ http://localhost:4000${server.graphqlPath}`)
    )

残りは以下の項目などを実装していくことになる.コード量が多く,今回は割愛するけど,より実践的な実装を学べるため,試しておくと良いかと!GitHub の完成形を見るだけでも雰囲気は伝わるはず.

  • typeDefsresolvers を別ファイルに分割して index.js をリファクタリングする
  • MongoDB を使ってデータを永続保存する
  • GitHub API を使って認証と認可を実装する

GraphQL サーバの実装を写経する : apollo-client

6章「GraphQLクライアントの実装」では,React から GraphQL を扱うために graphql-requestapollo-client を学ぶ.今回は個人的に興味のあった apollo-client を写経した.React 自体はあまり難しい点はなく,apollo-boost (apollo-client などを含む)react-apollo を使うことにより,実装がシンプルになることを体験できた.以下は実装した User.js の中で GraphQL Server にクエリを実行している部分を抜粋している.

// 中略

const Users = () => 
    <Query query={ROOT_QUERY} fetchPolicy="cache-and-network">
        {({ data, loading, refetch }) => loading ?
            <p>loading users...</p> :
            <UserList count={data.totalUsers} 
                users={data.allUsers} 
                refetch={refetch} />
        }
    </Query>

// 中略

本書を読んでいて参考になったのはキャッシュ実装の仕組みで,REST だとエンドポイントごとにキャッシュできるけど,GraphQL だと固定のエンドポイントになるため,どうキャッシュを実現するの?という話だった.react-apollo を使うと options.fetchPolicy という設定があり,以下の5種類から選べる.また apollo-cache-persist と組み合わせると,キャッシュを localStorage に保存することもできる.このあたりはプロダクションコードを実装するときに改めて検討したいと思う.

  • cache-first
  • cache-and-network
  • network-only
  • cache-only
  • no-cache

https://www.apollographql.com/docs/react/api/react-apollo/www.apollographql.com

とは言え,まだまだ apollo-client のメリットを学べてなく,引き続き調査をしていく.

実践投入

7章「GraphQLの実戦投入にあたって」では,これから本番環境に GraphQL を導入したい人と既に導入している人に最適な内容になっている.僕自身もまだ GraphQL はプロトタイプでしか使ってなく,知らない内容も多かった.

特に「漸進的なマイグレーション」という解説は良かった.どのように既存のアプリケーションを GraphQL に移行するか?という点で,計5種類の戦略が紹介されていた.GraphQL をゲートウェイのように使って,リゾルバから REST API にアクセスするパターンは,並行稼動を前提とした「移行のしやすさ」もあり,現場でも使う機会がありそうだった.

  • REST からリゾルバにデータをフェッチする
  • もしくは GraphQL リクエストを使用する
  • 1つか2つのコンポーネントに GraphQL を組み込む
  • 新しい REST エンドポイントを作成しない
  • 現在の REST エンドポイントをメンテナンスしない

誤植 : 初版第1刷

  • P.vii GrapghQLGraphQL
  • P.48 List 型のLift 型の
  • P.51 https://www.graphqlbin.com/v2/ANgjtr → 既に Server cannot be reached になっている
  • P.53 https://www.graphqlbin.com/v2/yoyPfz → 既に Server cannot be reached になっている
  • P.71 「構成されるでデータになり」→「構成されるデータになり」
  • P.75 DataTimeDateTime
  • P.82 「エラーが帰ってきます」→「エラーが返ってきます」
  • P.97 type Mutation { のインデント誤り
  • P.138「写真共有サービス」→「写真共有アプリケーション」
  • P.149「写真管理サービス」→「写真共有アプリケーション」
  • P.213 Amazon Web ServiceAmazon Web Services

なお,誤植ではないけど,P.7の「状態機械」はシンプルに「ステートマシン」で良さそうな気がする.

まとめ

  • 「初めての GraphQL」を読んだ
  • GraphQL 初学者をターゲットに網羅的に学ぶことができる1冊だった
    • 特に「背景 → クエリ → スキーマ → リゾルバ → クライアント → 実戦投入」という流れは素晴らしい!
  • 1周目は全体をザッと読みつつ,2周目で手を動かしながら読み直すのが,1番学習効率が高そう