20190411のReactに関する記事は6件です。

ReactとFirebaseUIで簡単Firebase Authentication

Webアプリ開発において認証周りは面倒くさく後回しにしがちですが、Firebase Authenticationを使うと簡単にユーザー認証が実装できたので共有しようと思います。

ついでにめんどくさいUIもFirebaseUIを使って楽していこうと思います。

React, firebaseインストール

この辺はたくさん情報があると思うので端折ります。

npm install --save firebase

firebaseフォルダを作成して設定しておきましょう。

firebase/config.js
export const firebaseConfig = {
    apiKey: "XXXXXXX",
    authDomain: "XXXXXXX",
    databaseURL: "XXXXXXX",
    projectId: "XXXXXXX",
    storageBucket: "XXXXXXX",
    messagingSenderId: "XXXXXXX",
};
firebase/index.js
import firebase from 'firebase';
import { firebaseConfig } from './config';

export const firebaseApp = firebase.initializeApp(firebaseConfig);

export default firebase;

FirebaseUIインストール

今回はUIも自動的に作成してもらうため、FirebaseUIをインストールします。
ありがたいことにReact用のラッパーが存在するのでこれを利用します。

npm install --save react-firebaseui

これを使ってログインフォームを表すコンポーネントを作ります

components/SignInScreen.jsx
import React from 'react';
import firebase from '../firebase';
import StyledFirebaseAuth from 'react-firebaseui/StyledFirebaseAuth';

const uiConfig = {
    signInFlow: 'popup',
    signInSuccessUrl: "/",
    signInOptions: [
        firebase.auth.GoogleAuthProvider.PROVIDER_ID,
        firebase.auth.FacebookAuthProvider.PROVIDER_ID,
        firebase.auth.TwitterAuthProvider.PROVIDER_ID,
        firebase.auth.GithubAuthProvider.PROVIDER_ID,
        firebase.auth.EmailAuthProvider.PROVIDER_ID,
        firebase.auth.PhoneAuthProvider.PROVIDER_ID,
        firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID
    ],
}


const SignInScreen = (props) => {
    return (
        <div>
            <p>Please sign-in:</p>
            <StyledFirebaseAuth uiConfig={uiConfig} firebaseAuth={firebase.auth()} />
        </div>
    );
}
export default SignInScreen;

どのサービスでログインするかはsignInOptionsで選択できます。適宜コメントアウトしてください。
このコンポーネントを使ってメインページを作成します。

App.jsx
import React, { Component } from 'react';
import firebase from './firebase';
import SignInScreen from './components/SignInScreen';

class App extends Component {
  state = {
    loading: true,
    user: null
  };

  componentDidMount() {
    firebase.auth().onAuthStateChanged(user => {
      this.setState({
        loading: false,
        user: user
      });
    });
  }

  logout() {
    firebase.auth().signOut();
  }

  render() {
    if (this.state.loading) return <div>loading</div>;
    return (
      <div>
        Username: {this.state.user && this.state.user.displayName}
        <br />
        {this.state.user ?
          (<button onClick={this.logout}>Logout</button>) :
          (<SignInScreen />)
        }
      </div>
    );
  }
}

export default App;

デモ

Googleとメールアドレス認証しか有効にしていませんが、以下のように表示されます。

ログイン前

スクリーンショット 2019-04-11 16.49.22.png

ログイン後スクリーンショット 2019-04-11 16.52.27.png

Firebaseコンソールからユーザー管理ができます。
スクリーンショット 2019-04-11 16.52.56.png

あまりにも簡単に認証が実現できました。Firebase大好きです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでつくる動的ページ

チュートリアルをもとに、Next.jsでの動的ページ作成方法を見ていきます。

チュートリアル用のリポジトリをクローンしておきましょう。

$ git clone https://github.com/zeit/next-learn-demo.git
$ cd next-learn-demo

インストールとルーティングの基礎はこちらからどうぞ。

クエリパラメーターで記事を表示する

サンプルのソースコードディレクトリに移動し、npm iしておきます。

$ cd 3-create-dynamic-pages
$ npm i

pages/index.jsでは投稿リストを表示し、遷移先のpages/post.jsではクエリパラメーターをもとに記事タイトルを表示します。
以下のように書き換えてみてください。

// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

// 各記事タイトルと遷移先のコンポーネント
const PostLink = props => (
  <li>
    <Link href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        {/* title属性がクエリパラメーターとして渡される */}
        <PostLink title="Hello Next.js" />
        <PostLink title="Learn Next.js is awesome" />
        <PostLink title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  )
}
// pages/post.js
import { withRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Page = withRouter(props => (
  <Layout>
    {/* props.router.query で ?title={hoge}を取得できます*/}
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </Layout>
))

export default Page

pages/post.js<Layout>の中身をコンポーネントにしたい場合は、以下のようにします。

// pages/post.js
import { withRouter } from 'next/router'
import Layout from '../components/MyLayout.js'

const Content = withRouter(props => (
  <div>
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </div>
))

const Page = props => (
  <Layout>
    <Content />
  </Layout>
)

export default Page

ルートマスキングによるクリーンなURL

Next.jsのルートマスキング機能を使うことにより、クリーンなURLを作成できます。

ディレクトリを移動して、npm iしておきましょう。

$ cd ../4-clean-urls
$ npm i

pages/index.jsを以下のように書き換えます。

// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'

// 各記事タイトルと遷移先のコンポーネント
const PostLink = props => (
  <li>
    {`/* asを使うことにより、hrefの記述をシンプルなものにすることができる */`}
    <Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
      <a>{props.title}</a>
    </Link>
  </li>
)

export default function Blog() {
  return (
    <Layout>
      <h1>My Blog</h1>
      <ul>
        <PostLink id="hello-nextjs" title="Hello Next.js" />
        <PostLink id="learn-nextjs" title="Learn Next.js is awesome" />
        <PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
      </ul>
    </Layout>
  )
}

<Link>コンポーネントのas<PostLink>コンポーネントのidを、hreftitleをそれぞれ渡しています。

// pages/post.jsから抜粋
const Page = withRouter(props => (
  <Layout>
    {/* props.router.query で ?title={hoge}を取得できます*/}
    <h1>{props.router.query.title}</h1>
    <p>This is the blog post content.</p>
  </Layout>
))

withRouterメソッドの引数propsprops.router.asPathをみると、pages/index.jsasを受け取っていることがわかります。hrefで渡しているクエリーパラメーターは?title={hoge}なので、props.router.query.titleでは、{hoge}の部分が表示されます。

ただし、この方法ですとクライアントサイドでのレンダリング結果なので、/p/{hoge}ページで再読込をすると404になってしまいます。それを避けるために、カスタムサーバーAPIを使います。

カスタムサーバーAPI

例によって、チュートリアルのディレクトリを移動しましょう。また、Expressを利用するので、一緒にインストールしてください。

$ cd 5-clean-urls-ssr
$ npm i
$ npm install --save express

server.jsを作成します。

const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    server.get('/p/:id', (req, res) => {
      const actualPage = '/post'
      const queryParams = { title: req.params.id }
      app.render(req, res, actualPage, queryParams)
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

また、package.jsonのnpm scriptsを以下のように書き換えます。

{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

今度は、/p/{hoge}で再読込しても404にならなくなりました。しかし、pages/index.jsから遷移してきたときと表示が異なってしまっているはずです。これは、/p/{hoge}ページだけではpages/index.jsから?title={hoge}を受け取ることができないからです。外部からAPIで情報を受け取る場合は、どちらも共通のIDを利用するため、これは問題にならないかと思われます。

外部APIから情報を取得する

チュートリアルではバットマンのAPIから情報を取得しているので、こちらもバットマンを呼んでみましょう。

ディレクトリを移動して、npm iしておきましょう。また、データを取得するためにisomorphic-unfetchもインストールしておきましょう。

$ cd ../6-fetching-data
$ npm i
$ npm install --save isomorphic-unfetch

pages/index.jsを以下のように書き換えます。

// pages/index.js
import Layout from '../components/MyLayout.js'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'

const Index = props => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(show => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
  const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
  const data = await res.json()

  console.log(`Show data fetched. Count: ${data.length}`)

  return {
    shows: data.map(entry => entry.show)
  }
}

export default Index

getInitialPropsメソッドは、静的な非同期関数です。初回の読み込み時、getInitialPropsはサーバーサイドで実行され、クライアント側のルーティングで遷移してきた場合はクライアントサイドで実行されます。関数内のconsole.logが、どのコンソールで表示されているかで確認できます。

pages/post.js側も対応しましょう。server.jsを以下に書き換えます。

// server.js
const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app
  .prepare()
  .then(() => {
    const server = express()

    server.get('/p/:id', (req, res) => {
      const actualPage = '/post'
      const queryParams = { id: req.params.id } // title: を id: に変更
      app.render(req, res, actualPage, queryParams)
    })

    server.get('*', (req, res) => {
      return handle(req, res)
    })

    server.listen(3000, err => {
      if (err) throw err
      console.log('> Ready on http://localhost:3000')
    })
  })
  .catch(ex => {
    console.error(ex.stack)
    process.exit(1)
  })

pages/post.jsも書き換えます。

// pages/post.js
import Layout from '../components/MyLayout.js'
import fetch from 'isomorphic-unfetch'

const Post = props => (
  <Layout>
    <h1>{props.show.name}</h1>
    <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
    <img src={props.show.image.medium} />
  </Layout>
)

Post.getInitialProps = async function(context) {
  const { id } = context.query
  const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
  const show = await res.json()

  console.log(`Fetched show: ${show.name}`)

  return { show }
}

export default Post

pages/post.jsでもバットマンの情報が表示されるようになりました。

前回のエントリーと合わせて、Next.jsの大枠は見えてきたかと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのまとめ(初心者)

未来電子テクノロジーでインターンをしているryota_miraidenshiです。
今回はReactについて勉強しているのでまとめておきたいと思います。

Reactって?

JavaScriptの最も人気なフレームワークがReactです。
なぜ人気なのでしょうか。

UIが作りやすい

Reactはユーザー目線で開発されているのでUIの機能追加に特化しているのに加えて、ユーザーがストレスを感じないように高速で処理が行われます。
では、高速処理がどのように行われているのか。
それはまだまだ理解できていない部分も多いのですが、ページ全体を変更するのではなく、変更があったところだけ変更を行うというやり方だからだそうです。

コンポーネント指向

これは、UIを部品化して管理、再利用しやすくすることができます。
HTMLのタグみたいな感じで自分で部品化できるので他のフレームワークには無い便利さがあると思います。

一度学べばどこでも使える

「React Native」というものを使って簡単にアプリなどを作成することができます。

まとめ

新しい言語、フレームワークなどを学ぶ際にはやれと指示されたからやるのではなく、それを学ぶことで何ができるようになるのか、なぜそれを選ぶのかを考えるとより一層理解できますし、楽しいはずです。
プログラム初心者であるため、内容に誤りがあるかもしれません。
もし、誤りがあれば修正しますのでどんどん指摘してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

graphql-codegenでgraphqlのendpointからTypescriptの型定義、さらにreact-apollo-hooksも自動生成

問題

Typescriptを使ってgraphqlは型定義が煩雑になる

  • graphqlのschemaで全て定義してるのに
  • 使う側のTypescriptで再度同じ定義する必要がある(variablesだったり、戻り値のオブジェクト定義だったり)

これをendpointのschemaや、実際に使っているgraphqlクエリから型定義を自動生成しようというのが↓

GraphQL Codegen

https://graphql-code-generator.com/docs/getting-started/

必要になるもの

  1. schema

  2. 使用する実際のgraphqlクエリファイル

    • https://graphql-code-generator.com/docs/getting-started/documents-field
    • (通常 const query = gql... で定義するものをファイルへ書き出す必要がある
    • 書き出さなくても、documents: src/**/*.{ts,tsx}のように設定してコード内のgraphqlクエリを読み出せるみたい
    • 実際には生成されるuseHookを使う事になり、そうなるとクエリをコード内に書く必要がなくなるので、どうしても最後には別ファイルにクエリを書き出す事になると思うが...
UserInfo.graphql 例
query UserInfo{
  name
  age
}

始め方

ドキュメントに沿ってやってみたが実際にはエラーで止まる箇所があるので、以下のように理解してやってみた。

まずは必要なパッケージのインストール

$ yarn add graphql
$ yarn add -D @graphql-codegen/cli

その後は次のコマンドでcodegen.ymlを自動生成できるが、これで自動生成されたものでは実際にはうまく動かないので、スキップ(v1.0.7現在)

$ graphql-codegen init # やらない

自分でcodegen.ymlを作る

overwrite: true # 出力するファイルを毎度上書きする
schema: "http://localhost:3000/graphql" # 自分のendpoint
documents: "**/*.graphql" # クエリファイル 拡張子は変えても良い
generates:
  src/generated/graphql.ts: # 出力するファイル名 
                            # 出力先、ファイル名は任意だが、拡張子はtsまたはtsx
    plugins: # 必要となるプラグイン
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      # 下の3つはtypescript-react-apolloのオプション
      withHOC: false # HOCは要らないので
      withComponent: false # falseなので出力ファイル拡張子が.tsでもいい。
                           # このオプションを設定しないとデフォルトtrueなので、.tsx拡張子にしろとエラーが出る
      withHooks: true # 上のdocumentsの設定があればこれでhookが自動生成される

参考:

pluginsに挙げたpackageはまだインストールしていないので、インストールする

yarn add -D @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

codegen.ymldocumentsに書いたパスへクエリファイルを配置する
(このクエリファイルが無いとwithHooks:trueでもhookは出力されないので注意)

例 src/documents/UserInfo.graphql
query UserInfo{
  name
  age
}

最後にyarn generateと簡単に出来るようにscriptを追加

package.json

  "scripts": {
    "generate": "graphql-codegen --config codegen.yml"
  }

そして実行

yarn generate

そうするとsrc/generated/graphql.tsが出力される
これにより今までTypescript側でも行っていた型定義が不要になる(importすればいいだけになる)
さらにdocumentsに書いたクエリ毎のuseHookも出来上がっているので、下のように簡単にgraphqlが使えるようになる

使用例

import { useGeneratedQuery } from "./src/generated/graphql"

...
...

export default () => {
  const query = useGeneratedQuery()

  if(query.loading){
    return 
  }

  return (
    <div>{query.data.name}</div>
  )
}

このhookに関してはデフォルトhooksImportFrom (string, default value: react-apollo-hooks))としてreact-apollo-hooksを使っているので、実際に使用する際には

$ yarn add react-apollo-hooks

が必要になる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQLでスキーマ駆動開発導入したら開発効率がアップするぞ!!

現在携わっているプロジェクトにGraphQL を使ってスキーマ駆動開発を導入したんですけど、かなり開発効率もよくなおかつ品質向上に良いのでやったことのまとめとして記事にしてみます。

これからスキーマ駆動開発を導入を検討している方の後押しになれば幸いです。

スキーマ駆動以前の開発だと何が辛いのか

どうしてもフロントエンド開発の着手が遅くなりがちという点があると思います。
基本的には設計 > API実装 > フロント実装という流れなんですよね。さらに開発を進めていく中でうまく行かない部分があればAPIの実装・修正が必要になる場合もあります。そうするとさらにフロントの実装は遅れてしまいます。

あとはAPIの実装がこんな感じになりそうだから、そうなるていでフロントも実装したりも。んで、あとで出戻りがあったりして開発効率としては良くないですね。

スキーマ駆動開発だと何が嬉しいのか

  • API開発とフロント開発を同時に進めることができる
  • スキーマがドキュメントとして存在するので出戻りがない(少ない)
  • スキーマを元に型定義するので品質もアップ

あとは直接スキーマ駆動開発の利点ではないのですが、後述の graphql-codegen を使うとコマンド一つでスキーマから型定義ファイルを生成することができ、さらに開発効率がアップします。

開発手順のおさらい

まずスキーマ駆動の開発手順としてはざっくり以下のよう流れになっています。

  1. スキーマの定義
  2. mock作成
  3. フロント、サーバ共に実装
  4. テスト or リリース

以下、開発手順と照らし合わせながら記事を進めます。

プロジェクトの構成図

基本方針として以下のことが決まっていました。

  • GraphQLを使う
  • サーバサイド(BFF)はnodeで
  • フロントはReact
  • すでにマイクロサービスはある

image.png

スキーマ

まずはスキーマの定義を行います。
例えばこんな感じで書きます。今回はわかりやすくUser一覧とUser単体の取得系とログインするスキーマを定義してみます。

schema.graphql
scalar Date
scalar DateTime
scalar Error
scalar EmailAddress
scalar URL

type Query {
  user(id: ID!): User
  users: [User]
}

type Mutation {
  login(email: String!, password: String!): AuthResponse!
}

type AuthResponse {
  success: Boolean
  error: Error
  token: String
}

type User {
  id: ID!
  name: String
  email: EmailAddress
  registerDate: DateTime
  profileImageUrl: URL
}

上記スキーマでOKならば次にqueryとmutationの作成をします。

user.graphql
query user($id: ID!) {
  user(id: $id) {
    id
    name
    profileImageUrl
    registerDate
  }
}

query users {
  users {
    id
    name
    registerDate
    profileImageUrl
  }
}

login.graphql
mutation loginStaff($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    token
  }
}

で次にTypeScriptであれば型の定義したり、resolverの型定義だったりしなければいけません。これが意外と面倒なんですよね。 graphql-codegen を使うことでこの辺りの面倒な作業を完全に省けます。

というのもスキーマさえあればコマンド一つでTSの型定義やらuseQuery(後述)、useMutation(後述)などを生成をしてくれます。これについてもスキーマ駆動開発との相性がよかったなと思っています。スキーマを定義したらフロントのViewでqueryとmutationが使える状態です。

詳しいやり方は公式をみてもらうとして

codegenの設定ファイルは以下のようにしました。出力先として server/client/ を指定しています。

codegen.yml
overwrite: true
schema: './graphql/schema.graphql' // スキーマの場所
documents: './graphql/**/*.graphql' //  queryとかmutationの定義の場所
generates:
  ../server/src/gen/types.ts: // 出力先 こっちはserver側
    plugins:
      - 'typescript'
      - 'typescript-resolvers'
  ../client/src/gen/actions.tsx: // 出力先 こっちはclient側
    plugins:
      - 'typescript'
      - 'typescript-operations'
      - 'typescript-react-apollo'
    config:
      withComponent: false
      withHooks: true
      withHOC: false

server/src/gen/types.ts

かなり省略してるけど概ねこんな感じで生成されます。

types.ts
export type AuthResponse = {
  success?: Maybe<Scalars['Boolean']>
  error?: Maybe<Scalars['Error']>
  token?: Maybe<Scalars['String']>
}

export type Mutation = {
  login: AuthResponse
}

export type MutationLoginArgs = {
  email: Scalars['String']
  password: Scalars['String']
}

export type Query = {
  user?: Maybe<User>
  users?: Maybe<Array<Maybe<User>>>
}

export type QueryUserArgs = {
  id: Scalars['ID']
}

export type User = {
  id: Scalars['ID']
  name?: Maybe<Scalars['String']>
  email?: Maybe<Scalars['EmailAddress']>
  registerDate?: Maybe<Scalars['DateTime']>
  profileImageUrl?: Maybe<Scalars['URL']>
}


export type MutationResolvers<Context = any, ParentType = Mutation> = {
  login?: Resolver<AuthResponse, ParentType, Context, MutationLoginArgs>
}

export type QueryResolvers<Context = any, ParentType = Query> = {
  user?: Resolver<Maybe<User>, ParentType, Context, QueryUserArgs>
  users?: Resolver<Maybe<Array<Maybe<User>>>, ParentType, Context>
}

export type UserResolvers<Context = any, ParentType = User> = {
  id?: Resolver<Scalars['ID'], ParentType, Context>
  name?: Resolver<Maybe<Scalars['String']>, ParentType, Context>
  email?: Resolver<Maybe<Scalars['EmailAddress']>, ParentType, Context>
  registerDate?: Resolver<Maybe<Scalars['DateTime']>, ParentType, Context>
  profileImageUrl?: Resolver<Maybe<Scalars['URL']>, ParentType, Context>
}

export type Resolvers<Context = any> = {
  AuthResponse?: AuthResponseResolvers<Context>
  Mutation?: MutationResolvers<Context>
  Query?: QueryResolvers<Context>
  User?: UserResolvers<Context>
}

export type IResolvers<Context = any> = Resolvers<Context>

client/src/gen/actions.ts

今回は、hooksのみで実装しようと思っていたので react-apollo-hooks 使います。codegen.yml の withHooks をtrueにすることで生成されます。Apolloのcomponentタイプなども選べるのでフロントの実装に合わせて生成が可能です。

actions.ts
type Maybe<T> = T | null
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string
  String: string
  Boolean: boolean
  Int: number
  Float: number
  EmailAddress: any
  DateTime: any
  URL: any
  Error: any
  Date: any
}

export type AuthResponse = {
  success?: Maybe<Scalars['Boolean']>
  error?: Maybe<Scalars['Error']>
  token?: Maybe<Scalars['String']>
}

export type Mutation = {
  login: AuthResponse
}

export type MutationLoginArgs = {
  email: Scalars['String']
  password: Scalars['String']
}

export type Query = {
  user?: Maybe<User>
  users?: Maybe<Array<Maybe<User>>>
}

export type QueryUserArgs = {
  id: Scalars['ID']
}

export type User = {
  id: Scalars['ID']
  name?: Maybe<Scalars['String']>
  email?: Maybe<Scalars['EmailAddress']>
  registerDate?: Maybe<Scalars['DateTime']>
  profileImageUrl?: Maybe<Scalars['URL']>
}
export type LoginMutationVariables = {
  email: Scalars['String']
  password: Scalars['String']
}

export type LoginMutation = { __typename?: 'Mutation' } & {
  login: { __typename?: 'AuthResponse' } & Pick<AuthResponse, 'token'>
}

export type UserQueryVariables = {
  id: Scalars['ID']
}

export type UserQuery = { __typename?: 'Query' } & {
  user: Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'profileImageUrl' | 'registerDate'>>
}

export type UsersQueryVariables = {}

export type UsersQuery = { __typename?: 'Query' } & {
  users: Maybe<Array<Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'registerDate' | 'profileImageUrl'>>>>
}

import gql from 'graphql-tag'
import * as ReactApolloHooks from 'react-apollo-hooks'

export const LoginDocument = gql`
  mutation login($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      token
    }
  }
`

export function useLoginMutation(
  baseOptions?: ReactApolloHooks.MutationHookOptions<LoginMutation, LoginMutationVariables>
) {
  return ReactApolloHooks.useMutation<LoginMutation, LoginMutationVariables>(LoginDocument, baseOptions)
}
export const UserDocument = gql`
  query user($id: ID!) {
    user(id: $id) {
      id
      name
      profileImageUrl
      registerDate
    }
  }
`

export function useUserQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UserQueryVariables>) {
  return ReactApolloHooks.useQuery<UserQuery, UserQueryVariables>(UserDocument, baseOptions)
}
export const UsersDocument = gql`
  query users {
    users {
      id
      name
      registerDate
      profileImageUrl
    }
  }
`

export function useUsersQuery(baseOptions?: ReactApolloHooks.QueryHookOptions<UsersQueryVariables>) {
  return ReactApolloHooks.useQuery<UsersQuery, UsersQueryVariables>(UsersDocument, baseOptions)
}

mockサーバー

Apollo Serverで簡単に作れます。てかこれだけです。
さらに、レスポンスをカスタマイズも簡単にできます。詳しいやり方はMocking - Apollo Docsを見てみてくださいね。

const { ApolloServer, gql } = require('apollo-server');

const typeDefs = gql`
  type Query {
    hello: String
  }
`;

const server = new ApolloServer({
  typeDefs,
  mocks: true,
});

server.listen().then(({ url }) => {
  console.log(`? Server ready at ${url}`)
});

マイクロサービス

User, Admin ・・・といった具合にマイクロサービス化しています。

BFF (バックエンド For フロントエンド)

簡単に技術スタックをご紹介すると・・・
Node + koa + Apollo + TypeScript って感じです。GraphQLをBFFで使うこともできたのもマイクロサービスがすでにあったのが大きいですね。今回のプロジェクトにGraphQLはかなりマッチしていました。

Apollo-Serverを使ってGraphQLサーバを立ち上げるのですが、http://localhost:4000/graphql など任意のURLでGraphiQL IDEが立ち上がるのでブラウザ上で確認をしながら実装することができます。

const Query: QueryResolvers = {
  talent(obj, args, context, info) {
    // マイクロサービスからかき集めてデータを返す
  },
  users(obj, args, context, info) {
    // マイクロサービスからかき集めてデータを返す
  }
}

const Mutation: MutationResolvers = {
  async login(_obj, arg: { email: string; password: string }) {
    const { password, email } = arg
    // 何かしらの処理
  }
}

const resolvers: Resolvers = {
  Query,
  Mutation,
}

export default resolvers

フロントエンド

React + Apollo + TypeScript で実装をしています。

実際の開発の流れとしてはまずは前述のmockサーバで実装、resolverができたらdevサーバで開発・確認をして進めます。

さらに、今回はwithHooksを使って取得・更新をするようにしました。graphql-codegenuseUsersQueryuseLoginMutation を生成してくれるのでフロントでは使うだけです。このwithHooksが結構よかったですね。Componentと機能ごとに作ることができその中に閉じ込めることができます。個人的にはAtomic Designと合わせるさらに良いかなと。

const Users: React.FunctionComponent<{}> = () => {
  const { data, loading } = useUsersQuery()
  if (loading) return <div>Loading</div>
  if (Object.keys(data).length < 1) return null
  return (
    <React.Fragment>
      {data.talents.map((e, key) => {
        return (
          <div>
            // 何かしら表示
          </div>
        )
      })}
    </React.Fragment>
  )
}
const LoginContent: React.FunctionComponent<{}> = () => {
  const onSubmitSignIn = useLoginMutation({
    update: (_, { data }) => {
      // mutationでsuccess後の処理
    }
  })

  return (
    // form
  )
}

余談だけどReduxはいらない

GraphQLを使うことでReduxはいよいよ必要ないかなと。そもそもサーバ側とReduxで同じデータを管理していたようなものですし。さらに、GraphQLというかこの場合はApolloなのかな、エラーハンドリングも UNAUTHENTICATEDINTERNAL_SERVER_ERROR のコードを返してくれるので、Toastなんかと組み合わせてそこまでコストかからずに実装できます。

テストについて

基本的にスキーマから生成しているものなので、型やデータの整合性は取れています。さらにGraphQLエラーとしてValidationもしてくれるので、従来の開発スタイルよりもかなり品質は向上しますね。

ただ、ログインのMuationのようにemailやpasswordをvariablesとして渡す必要がある場合にはスキーマの変更がフロントまで行き届かないです。

例えばスキーマで email > emailAddress というように変更があったとして、型定義やらwithHooksなどは生成し直してくれるわけなんですが、View側で

useLoginMutation({variables: {email: '', password: ''}})

としている場所は勝手に変更されることはないです。ま、当然といえば当然何ですが。あまり頻発するようなことはないでしょうけどこのあたりの変更をテストで間違ってリリースしてしまうのを防ぐ必要はありそうです。

スナップショットなどをとってViewの差分を見ても良いんですけど、これはリリースした後にゆっくり導入していければ。

色々試したんですけど、 easygraphql-tester 一番簡単でわかりやすかったので。あとはCircle Ciなど使ってテストしていければいいですよね。

describe('A user', function() {
  let tester
  beforeAll(() => {
    tester = new EasyGraphQLTester(schemaCode)
  })

  test('UsersDocument', () => {
    tester.test(true, UsersDocument)
  })
  test('UserDocument', () => {
    tester.test(true, UserDocument, { id: 1 })
  })
  test('LoginDocument', () => {
    tester.test(true, LoginStaffDocument, { email: 'example@gmail.com', password: 'hgoehoge' })
  })
})

とはいえ問題点もある

プロジェクトリリース後にスケールした時や変更があった時ににスキーマ管理(バージョン含め)が今後のリリース後の課題感としてチームで話が上がっています。その辺りも今後記事にできればと思っています。

とはいえGraphQLを初めてプロジェクトに導入した結果、スキーマ駆動開発のおかげで作業効率もよく、品質が向上したのは良いことだと思っています。

BFFとの相性が良いのは実感できたが、そうじゃない場合はどうなんだろってのはあるので機会があれば試したいみたいです。

今後GraphQLの事例がもっと増えてくれると良いなと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsbyのチュートリアルのまとめ(3章-1)

1章2章 に引き続き、Gatsby公式サイトのチュートリアルを紹介していきます。今回は3章ですが、長いので2つの記事に分割してお送りします。

3. Creating nested layout components

Creating nested layout components | GatsbyJS

3章はプラグインの使い方とlayout componentについて学ぶ章です。今回の記事では前半であるプラグインの使い方について紹介です。軽くまとめると以下の感じです。

  • Gatsbyではプラグインを利用することができる。プラグインはnpmでインストールする
  • 利用するプラグインはgatsby-config.jsで記述し、それぞれ個別の設定ファイルを用意する

Gatsbyプラグイン

Gatsbyは機能を拡張するプラグインに対応しています。プラグインの一覧は公式サイトのプラグイン情報ページで確認することができます。

このチュートリアルでは、フォントのスタイルをテーマとして扱い、簡単に適用できるようにするJavaScriptライブラリTypography.jsをGatsbyで利用するプラグイン「gatsby-plugin-typography」をインストールし、使い方を学びます。

前準備

チュートリアルのため、新しいプロジェクトを作成します。

gatsby new tutorial-part-three https://github.com/gatsbyjs/gatsby-starter-hello-world
cd tutorial-part-three

gatsby-plugin-typographyのインストールと設定

作成したチュートリアル用プロジェクトのディレクトリで、npmによりプラグイン「gatsby-plugin-typography」をインストールします。

npm install --save gatsby-plugin-typography react-typography typography typography-theme-fairy-gates

プロジェクトのディレクトリ直下に、gatsby-config.jsを作成します。

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
  ],
}

gatsby-config.jsは前章で出てきたgatsby-browser.js同様特殊なファイルで、プラグインやその他サイト設定などを記述するファイルとのことです。詳細は下記ドキュメントに記載されています。ここでは使用するプラグインとしてgatsby-plugin-typographyを設定しています。

Gatsby Config | GatsbyJS

さて、上記の設定には「PathToConfigModule」というフィールドがあります。プラグインの設定ファイルの指定なのですが、このファイルはまだ存在しません。というわけでsrc/utils下にtypography.jsを下記内容で作成します。
(utilsディレクトリはデフォルトでは存在しないので作成します)

src/utils/typography.js
import Typography from "typography"
import fairyGateTheme from "typography-theme-fairy-gates"

const typography = new Typography(fairyGateTheme)

export const { scale, rhythm, options } = typography
export default typography

上記では「Fairy Gates」というテーマのスタイルを利用するよう設定しています。typography.jsのテーマがどんなものがあるか確認したい方は、typegraphy.jsの公式サイトの右側にあるメニューの「Pick theme」でいろいろなテーマを適用できるので、試してみると良いでしょう。

この状態でgatsby developを実行し、http://127.0.0.1:8000 にアクセスしてページを確認してみます。

gatsby develop

この状態ではHello Worldしか表示されないので分かりにくいですが、デベロッパーツールで確認してみると「typography.js」というidのCSSスタイルが設定されていることが分かると思います。

typography-styles.png

というわけで、これでプラグイン「gatsby-plugin-typography」を適用できました。

以上が3章の前半の内容になります。

3章の後半(次回)は、各ページで共通して利用できるlayout componentについてのチュートリアルです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む