- 投稿日:2019-04-11T16:55:46+09:00
ReactとFirebaseUIで簡単Firebase Authentication
Webアプリ開発において認証周りは面倒くさく後回しにしがちですが、Firebase Authenticationを使うと簡単にユーザー認証が実装できたので共有しようと思います。
ついでにめんどくさいUIもFirebaseUIを使って楽していこうと思います。
React, firebaseインストール
この辺はたくさん情報があると思うので端折ります。
npm install --save firebasefirebaseフォルダを作成して設定しておきましょう。
firebase/config.jsexport const firebaseConfig = { apiKey: "XXXXXXX", authDomain: "XXXXXXX", databaseURL: "XXXXXXX", projectId: "XXXXXXX", storageBucket: "XXXXXXX", messagingSenderId: "XXXXXXX", };firebase/index.jsimport 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.jsximport 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.jsximport 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とメールアドレス認証しか有効にしていませんが、以下のように表示されます。
ログイン前
ログイン後
![]()
あまりにも簡単に認証が実現できました。Firebase大好きです。
- 投稿日:2019-04-11T15:06:32+09:00
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を、hrefにtitleをそれぞれ渡しています。// 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メソッドの引数propsのprops.router.asPathをみると、pages/index.jsのasを受け取っていることがわかります。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の大枠は見えてきたかと思います。
- 投稿日:2019-04-11T13:53:26+09:00
Reactのまとめ(初心者)
未来電子テクノロジーでインターンをしているryota_miraidenshiです。
今回はReactについて勉強しているのでまとめておきたいと思います。Reactって?
JavaScriptの最も人気なフレームワークがReactです。
なぜ人気なのでしょうか。UIが作りやすい
Reactはユーザー目線で開発されているのでUIの機能追加に特化しているのに加えて、ユーザーがストレスを感じないように高速で処理が行われます。
では、高速処理がどのように行われているのか。
それはまだまだ理解できていない部分も多いのですが、ページ全体を変更するのではなく、変更があったところだけ変更を行うというやり方だからだそうです。コンポーネント指向
これは、UIを部品化して管理、再利用しやすくすることができます。
HTMLのタグみたいな感じで自分で部品化できるので他のフレームワークには無い便利さがあると思います。一度学べばどこでも使える
「React Native」というものを使って簡単にアプリなどを作成することができます。
まとめ
新しい言語、フレームワークなどを学ぶ際にはやれと指示されたからやるのではなく、それを学ぶことで何ができるようになるのか、なぜそれを選ぶのかを考えるとより一層理解できますし、楽しいはずです。
プログラム初心者であるため、内容に誤りがあるかもしれません。
もし、誤りがあれば修正しますのでどんどん指摘してください。
- 投稿日:2019-04-11T10:29:06+09:00
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/
必要になるもの
schema
- graphqlのendpoint(apollo-clientで接続するURL)
- GraphQL Introspection JSONでのschemaファイル
- schemaファイル
- のいずれか
使用する実際の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が自動生成される参考:
- https://graphql-code-generator.com/docs/getting-started/codegen-config
- https://graphql-code-generator.com/docs/getting-started/schema-field
- URL
- GraphQL Introspection JSON https://graphql.org/learn/introspection/
- graphql schema ファイル https://graphql.org/learn/schema/
- https://graphql-code-generator.com/docs/getting-started/documents-field
- https://graphql-code-generator.com/docs/plugins/typescript
- https://graphql-code-generator.com/docs/plugins/typescript-operations
- https://graphql-code-generator.com/docs/plugins/typescript-react-apollo
pluginsに挙げたpackageはまだインストールしていないので、インストールする
yarn add -D @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
codegen.ymlのdocumentsに書いたパスへクエリファイルを配置する
(このクエリファイルが無いとwithHooks:trueでもhookは出力されないので注意)例 src/documents/UserInfo.graphqlquery 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が必要になる
- 投稿日:2019-04-11T09:50:55+09:00
GraphQLでスキーマ駆動開発導入したら開発効率がアップするぞ!!
現在携わっているプロジェクトに
GraphQLを使ってスキーマ駆動開発を導入したんですけど、かなり開発効率もよくなおかつ品質向上に良いのでやったことのまとめとして記事にしてみます。これからスキーマ駆動開発を導入を検討している方の後押しになれば幸いです。
スキーマ駆動以前の開発だと何が辛いのか
どうしてもフロントエンド開発の着手が遅くなりがちという点があると思います。
基本的には設計 > API実装 > フロント実装という流れなんですよね。さらに開発を進めていく中でうまく行かない部分があればAPIの実装・修正が必要になる場合もあります。そうするとさらにフロントの実装は遅れてしまいます。あとはAPIの実装がこんな感じになりそうだから、そうなるていでフロントも実装したりも。んで、あとで出戻りがあったりして開発効率としては良くないですね。
スキーマ駆動開発だと何が嬉しいのか
- API開発とフロント開発を同時に進めることができる
- スキーマがドキュメントとして存在するので出戻りがない(少ない)
- スキーマを元に型定義するので品質もアップ
あとは直接スキーマ駆動開発の利点ではないのですが、後述の graphql-codegen を使うとコマンド一つでスキーマから型定義ファイルを生成することができ、さらに開発効率がアップします。
開発手順のおさらい
まずスキーマ駆動の開発手順としてはざっくり以下のよう流れになっています。
- スキーマの定義
- mock作成
- フロント、サーバ共に実装
- テスト or リリース
以下、開発手順と照らし合わせながら記事を進めます。
プロジェクトの構成図
基本方針として以下のことが決まっていました。
- GraphQLを使う
- サーバサイド(BFF)はnodeで
- フロントはReact
- すでにマイクロサービスはある
スキーマ
まずはスキーマの定義を行います。
例えばこんな感じで書きます。今回はわかりやすくUser一覧とUser単体の取得系とログインするスキーマを定義してみます。schema.graphqlscalar 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.graphqlquery user($id: ID!) { user(id: $id) { id name profileImageUrl registerDate } } query users { users { id name registerDate profileImageUrl } }login.graphqlmutation 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.ymloverwrite: 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: falseserver/src/gen/types.ts
かなり省略してるけど概ねこんな感じで生成されます。
types.tsexport 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.tstype 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-codegenでuseUsersQueryやuseLoginMutationを生成してくれるのでフロントでは使うだけです。この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なのかな、エラーハンドリングも
UNAUTHENTICATEDやINTERNAL_SERVER_ERRORのコードを返してくれるので、Toastなんかと組み合わせてそこまでコストかからずに実装できます。テストについて
基本的にスキーマから生成しているものなので、型やデータの整合性は取れています。さらにGraphQLエラーとしてValidationもしてくれるので、従来の開発スタイルよりもかなり品質は向上しますね。
ただ、ログインのMuationのようにemailやpasswordをvariablesとして渡す必要がある場合にはスキーマの変更がフロントまで行き届かないです。
例えばスキーマで
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の事例がもっと増えてくれると良いなと。
- 投稿日:2019-04-11T09:15:58+09:00
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-threegatsby-plugin-typographyのインストールと設定
作成したチュートリアル用プロジェクトのディレクトリで、npmによりプラグイン「gatsby-plugin-typography」をインストールします。
npm install --save gatsby-plugin-typography react-typography typography typography-theme-fairy-gatesプロジェクトのディレクトリ直下に、gatsby-config.jsを作成します。
gatsby-config.jsmodule.exports = { plugins: [ { resolve: `gatsby-plugin-typography`, options: { pathToConfigModule: `src/utils/typography`, }, }, ], }gatsby-config.jsは前章で出てきたgatsby-browser.js同様特殊なファイルで、プラグインやその他サイト設定などを記述するファイルとのことです。詳細は下記ドキュメントに記載されています。ここでは使用するプラグインとしてgatsby-plugin-typographyを設定しています。
さて、上記の設定には「PathToConfigModule」というフィールドがあります。プラグインの設定ファイルの指定なのですが、このファイルはまだ存在しません。というわけでsrc/utils下にtypography.jsを下記内容で作成します。
(utilsディレクトリはデフォルトでは存在しないので作成します)src/utils/typography.jsimport 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スタイルが設定されていることが分かると思います。
というわけで、これでプラグイン「gatsby-plugin-typography」を適用できました。
以上が3章の前半の内容になります。
3章の後半(次回)は、各ページで共通して利用できるlayout componentについてのチュートリアルです。




