全集中 Reactの呼吸 Relayの舞 『戯れ』
はじめに
こちらは『DMMグループ '20卒内定者 Advent Calendar 2019'』21日目の記事になります。
本記事は投稿主がNext(React)でGraphqlを使いたいが「apollo多いよね。relay使ってみたくない?」というお気持ちでreact-relayを用いた スターターキットを作り戯れる話 です。
- はじめに
- GraphqlとRelayについて
- Apolloはよく聞くけど、Relayはあまり聞かないよね。
- スターターキットを作る
- 感想
- 参考リポジトリ
- その他
GraphqlとRelayについて
Graphql
TOPかっこいいですね。非常にクールです。
GraphqlはRESTの課題解決をしようぜというお気持ちから生まれた規格です。 解決したい課題としてはいくつかありますが、投稿主が使っていて感じている利点として 必要なリソースを1つのリクエストで得られるという点にあるのかなと。
おかげさまで幾度もフェッチせずに済み、必要なデータもコンポーネントごとにfragment(Graphqlクエリの断片化)を定義することでDX最高です。
Relay
Graphqlのクールさとは相まって、ポップなTOPが良いですね。
Relayは単一のGraphqlリクエストで結果を得られるようにしたり、Mutation(データの作成・更新)も簡易的に実行できるようサポートされているGraphqlクライアントです。ありがたい。
もちろん、Graphqlクライアントということで以下のようなメリットがあります。
- コレクションのページネーション
- データキャッシュ
- Mutation後にキャッシュの一貫性保持
また、Relayには制限もあります。
- 採用技術がReact or ReactNativeになる。
- 導入するアプリケーション構造の自由度を下げる。
- Relay Server Specificationの規約でクエリとレスポンスの冗長性が少し増す。
Apolloはよく聞くけど、Relayはあまり聞かないよね。
日本記事の検索("react graphql")
アポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロ。
海外記事の検索("how to use graphql with react")
ようやく”Relay”の単語が出てきましたね!ちなみに、この記事の内容はApolloでした。
GraphqlクライアントにApolloが多い理由
単純な理由がいくつかあります。
- Apolloは様々な環境で使用できるが、RelayはReactがメインである。
- Apolloは軽量で柔軟なアプローチができるが、Relayは自由度が低い。
- Apolloはとにかく資料が豊富でキャッチアップが早いが、Relayの資料は少ない。
Apolloはこれから学習するので詳細には語れませんが、細かい比較内容をそのうち記事化します。
現在ではVue.jsでもRelayを導入する個人制作のパッケージ(vue-relay)などがありますが、やはり公式としてはReactオンリーのようです。
スターターキットを作る
0. 前提
フロントエンド構成は以下を利用する。
- TypeScript: ^3.7
- Next: ^9.1
- React
- nodenv
- react-relay
- relay-runtime
- relay-compiler
- @graphql-codegen
- @material-ui など
1. フロントエンド構成の説明
Next.js
今回、フロントエンドのフレームワークはZeit社のNext.jsを使用していきます。NextはReactを包含してSSRを可能とする素敵な技術ですが、仕事先のプロジェクトでがっつり使っていくということで今回はReact単体ではなくNextも含めて採用しました。
TypeScipt(3.7系)
Graphqlの型システムを利用するにはこれしかないでしょ!ということでいつものTypeScriptです。3.7系からOptional Chainingが利用できるという点も含めて最新を採用します。
nodenv
言わずもがな、nodenv。 プロジェクトごとにnodeバージョンを切り替えます。
react-relay, relay-runtime, relay-compiler
ReactでRelayを使用するコアパッケージや、Graphqlのスキーマファイルから各コンポーネントに対応する型定義ファイルを自動生成してくれる方々です。
@graphql-codegen
GraphqlスキーマファイルからTSの型定義ファイルを生成することに利用します。便利。
@material-ui
毎度お馴染み、Material Designのパッケージです。
2. Next + TSの環境作成
ざっとこんな感じの構成を作りました。 尚、Nextはpagesディレクトリなど、Next規定のディレクトリやファイルを作成すればちゃんと認識してくれます。
次セクションでディレクトリやファイルをピックアップして解説します。
project/ ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.tsx │ └── index.tsx ├── public │ ├── fonts │ ├── icons │ └── images ├── src │ ├── components │ ├── layouts │ ├── lib │ ├── styles │ └── types │ └── tsconfig.json
next.config.js
Nextの設定ファイルになります。Nuxtでいうnuxt.configのようなものになります。
- Dotenv
環境を振り分けるケースがわりかし多かったので入れました。
注意点としては通常のDotenvではなくdotenv-webpack
を使用することになる点です。
- withSass
今回はsrc/styles
にスタイリングファイル(Sass)を分離し、CSS moduleとして各コンポーネントで利用したかったので使用しました。
const withSass = require('@zeit/next-sass'); const Dotenv = require('dotenv-webpack'); const path = require('path'); module.exports = withSass({ cssModules: true, sassLoaderOptions: { includePaths: ["src/styles/**/*.sass"] }, webpack: config => { config.plugins = config.plugins || []; config.plugins = [ ...config.plugins, new Dotenv({ path: path.join(__dirname, '.env'), systemvars: true }) ]; return config } });
pages/
Nextがページとして認識するディレクトリです。 各ページファイルだけでなく、Next規定のファイル(_hogehoge.tsx)も設置します。
public/
アセットファイルを設置するディレクトリです。 以前はstaticでしたが、最近のバージョンではpublicに名を変えています。
src/components
コンポーネントを格納します。 Graphqlのfragment(断片化されたクエリ)を定義したコンポーネントなども含めるため、relay-compilerに読み取らせると各fragmentコンポーネントに必要な型定義が生成されます。 こんな感じ。
src/layouts
ページのレイアウトファイルを格納しています。 headerやfooterをセットにしたファイルですね。
src/lib
material-uiのテーマファイルやRelayのコンフィグ、ユーティリティなどを格納します。
src/styles
CSS moduleとして使用するSassファイルを格納します。 ここの細かい構造はまた今度。
3. Graphqlスキーマの生成
Graphqlのエンドポイントからスキーマファイルを生成するためget-graphql-schemaをインストールします。
使用
インストール後、Graphqlエンドポイントへ向けてスキーマファイルを生成するためのコマンドを実行します。
> get-graphql-schema <endpoint_url> schema.graphql
するとsrc/types/schema.graphql
が1秒もかからずにファイルが生成され、以下のようになります。
type User implements Node { id: ID! user: User! } """The connection type for History.""" type UserConnection { """A list of edges.""" edges: [UserEdge] """Information to aid in pagination.""" pageInfo: PageInfo! totalCount: Int! } ... // スキーマが続く。すごいぞ。
4. コードジェネレーターの導入
Graphqlスキーマファイルから型定義ファイルを自動生成するため@graphql-codegenを導入します。 ひとまず、この辺りのパッケージを入れておきました。
"@graphql-codegen/cli": "^1.9.1", // 必須 "@graphql-codegen/typescript": "^1.9.1", //必須 "@graphql-codegen/typescript-operations": "^1.9.1", "@graphql-codegen/typescript-resolvers": "^1.9.1",
また、自動生成にあたってのyamlを定義しなければならないのですが、最初は特にこだわることもないかなと思いますので以下のようにシンプルな定義にします。
overwrite: true schema: "schema.graphql" generates: src/types/graphql.ts: plugins: - typescript - typescript-resolvers
使用
諸々が整った段階でコマンドを実行します。いざ、ジェネレート。
> graphql-codegen --config codegen.yml
するとスキーマからsrc/types/graphql.ts
が生成され、スキーマの型定義ファイルとして使用できるようになります。このような感じ。openAPIと同様ですね。
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; export type Maybe<T> = T | null; export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } & { [P in K]-?: NonNullable<T[P]> }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string, String: string, Boolean: boolean, Int: number, Float: number, /** An ISO 8601-encoded datetime */ ISO8601DateTime: any, }; ... //型定義が続く。強いぞジェネレータ。
5. RelayのEnvironmentファイル作成
クライアント-Graphqlエンドポイント間での通信やレスポンスをキャッシュするためのストアなど、Relayをフルに使っていくために定義をする必要があります。
import {Environment, Network, RecordSource, Store} from 'relay-runtime'; export const fetchQuery = (operation: any, variables: any) => { return fetch(process.env.RELAY_ENDPOINT as string, { method: 'POST', body: JSON.stringify({ query: operation.text, variables, }), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, }).then((response: Response) => { if (response.ok) { return Promise.reject(response.statusText); } return response.json() }); }; const environment = new Environment({ network: Network.create(fetchQuery), store: new Store(new RecordSource()) }); export default environment;
ここで重要なのが以下の2つ。
NetWork
- QueryRendererコンポーネントからGraphqlのクエリや変数を受け取り通信を行う。
Store
- レスポンスを格納しキャッシュ、ローカルステートとして利用できるようにしてくれる。
6. Envファイル作成と完成
最後に、エンドポイントは様々なのでenvファイルで対応しておきましょう。
RELAY_ENDPOINT="http://localhost:xxxx/graphql"
以上でキットが完成しました。 そして、このキットを用いることでGraphql + Reactの開発を開始することができます。やったね。
感想
環境構築から既にしんどい思いをしたので、プロジェクトの立ち上がりがそこそこ遅いです。ですので、やるならばしっかりとスターターキットを作るべきだと感じました。
問題は土台のメンテナンスコストが負担になる点でしょうか。 環境構築の時点で非常に多くのパッケージを入れたかと思います。
また、Apolloも使ってみてどちらを採用するのか検討していきたいと思いますが、その前にせっかくRelayのキットを作ったので、 次回はキットを用いてコンポーネントを実装していく記事を書いていきたいと思います。
ではまた。
参考リポジトリ
本日のスターターはこちら。 まだ作っただけですので、READMEやexampleなど整備していきます。
その他
Githubが公式でGraphqlのお試しを用意しているので、触ってみたい方はぜひ。 Docsをクリックすると叩けるクエリなどが表示されます。(これ、なんと自分らのクライアントでもできてしまうんです。)
あ、クエリの叩き方はご自分で勉強してくださいね。
Vue3到来記念。Vueのアレって結局なんだっけシリーズ。〜render関数 編〜
はじめに
こちらは『DMMグループ '20卒内定者 Advent Calendar 2019』8日目の記事になります。
初アドカレ&初個人ブログということで気楽に発信していきたいと思います。
実装のみを見たい方は以下「render関数でIconコンポーネントを作る(準備編)」からご覧ください。
ご挨拶
日頃よりお世話になっております。tettyです。
今回は私の自己紹介+αですが、大体の自己紹介はもう書いてしまったんですよね...。
というわけで、今回は自己紹介のうち 技術・業務的なこと にフォーカスすることにしましょう。
自己紹介
はい。てってぃと申します。 自己紹介にはあまり興味がなく、早く主題に移りたいので簡単に。
1. Web Developerとして活動
- Web Developerを目指してかれこれ3年ほど頑張っている。
- メイン1社、お手伝い2社に技術者として関わる。
2. 立ち上げ開発・プロダクト検証がお好き
- 好みof好み。テックを経て世界を知る=自己満足感。
- 主にフロントエンドの技術選定から構築・実装まで行う。
- サーバサイドを担当することはもちろんある。
3. UXエンジニアもまた目標
- これもまた目指して頑張っている。
- 20代半ばくらいにUXうるさいおじさんになりたい。
- 日々自分の作ったプロダクトにクレームを入れている。
4. 最近の活動
- Next.js + TypeScript + Rails5 + Graphqlでプロダクト開発
- 直近の目標はExpress + GraphqlでBFFを作ること。難しい。
- 楽しい👏
- Nuxt.js + TypeScript + PWA(SW)+ FireStoreで卒業制作を開発
- KVSとJS→TSへの移行がとてもつらみ。
- ネイティブアプリのUIやアニメーションの実装にチャレンジしている。難しい。
- 楽しい👏
- Rails5 + S3 + Lambdaでスタートアップのプロダクト開発
- PM業と開発、ドメイン知識のインプット、接待を並行でやることがとてもつらみ。
- 楽しい👏
- DMM.comへの内定承諾について
- 一瞬すぎたのでお話しすることがあまりなく。
- 別記事でまとめたいと思います。
本題:render関数ってなんだっけ?
Vue.jsも少しばかり時間が経ってきたのかな?と思うこの頃ですが、ご存知の方よりも知らない(使ったことない)という方が多いであろう render関数 についてお話します。
1. そもそも
まずrender関数が目に入るのは main.jsですが、それがこちら。
// main.js new Vue({ render: h => h(App) }).$mount("#app");
@vue/cliでVueプロジェクトをインストールすると作成されるものになりますが、この中に記述されている render: h => h(App) って何?他で使うタイミングあるの?
2. render関数
私がVueを使い始めたのは1年と少し前。「は〜。renderはAppコンポーネントを描画する?関数なんだな〜。h?なにこれ???HOCのh???」という第一印象になりました。
こちら、render関数を追っていくことで答えを見つけることができます。
// render関数 render?(this: undefined, createElement: CreateElement, context: RenderContext<Props>): VNode | VNode[]; // hのやつ export interface CreateElement { (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), children?: VNodeChildren): VNode; (tag?: string | Component<any, any, any, any> | AsyncComponent<any, any, any, any> | (() => Component), data?: VNodeData, children?: VNodeChildren): VNode; }
型の通りですが「h」なる存在はcreateElementのこと。
htmlタグもしくはVueコンポーネントなどを渡すと仮装DOMを返却してくれる関数だとわかります。(ちなみにhはhyperscriptの略だそう。)
3. 過去の過ち
render関数についてまとめようと思った動機ですが、自分の過去リポジトリを漁っていてとんでもないものを見つけてしまったことにあります。それがこちら。
<template> <div> <IconAdd v-if="name === 'add'" /> <IconAlert v-if="name === 'alert'" /> <IconBlock v-if="name === 'block'" /> <IconFace v-if="name === 'face'" /> <IconFile v-if="name === 'file'" /> <IconLock v-if="name === 'lock'" /> <IconPerson v-if="name === 'person'" /> <IconTime v-if="name === 'time'" /> ...
「う゛!!!!!!!!!!!!!!!!!!!」
何がとんでもないのかよくお分かりかと思います。 これでは、選ばれなかったアイコン達がHTML自体にコメントとしてレンダリングされてしまいますね。パフォーマンスもよくありません。
さて、この暗黒物質を綺麗にしていきましょう。
render関数でIconコンポーネントを作る(準備編)
0.環境
1. ファイル構成
Iconコンポーネントを作成する際の構成は以下でいきます。
components ├── Example.vue ├── atoms │ └── IconTextField.vue └── icon ├── Icon.js ├── icons │ ├── IconCheckbox.vue │ ├── IconDoneOutline.vue │ ├── IconFace.vue │ ├── IconLock.vue │ ├── IconMoreHoriz.vue │ └── index.js └── types.js
2. アイコンに使いたいSVGをMaterial Designから拝借してきます。
3. SVGをVueコンポーネント化します。
// icon/icons/IconLock.vue <template> <svg xmlns="http://www.w3.org/2000/svg" :width="size" :height="size" :viewBox="viewBox" > <path d="M0 0h24v24H0z" fill="none" /> <path :style="style" d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z" /> </svg> </template> <script> import { iconColors, typeColor } from "../types"; export default { name: "IconLock", props: { size: { type: Number, default: 24 }, color: { type: String, default: "primary", validator: v => typeColor.includes(v) } }, methods: { getIconColor() { return iconColors[this.color] || iconColors.primary; } }, computed: { style() { return { fill: `${this.getIconColor(this.color)}` }; }, viewBox() { return `0 0 ${this.size} ${this.size}`; } } }; </script>
4. 作成したアイコン達をまとめます。
// icon/icons/index.js import IconFace from "./IconFace"; import IconDoneOutline from "./IconDoneOutline"; import IconMoreHoriz from "./IconMoreHoriz"; import IconCheckbox from "./IconCheckbox"; import IconLock from "./IconLock"; export default { checkbox: IconCheckbox, doneOutline: IconDoneOutline, face: IconFace, moreHoriz: IconMoreHoriz, lock: IconLock };
5. Iconコンポーネントに必要な属性値やカラーを用意します。
// icon/types.js export const iconColors = { primary: "#2196F3", secondary: "#607D8B", danger: "#f44336", warning: "#FF9800" }; export const typeColor = ["primary", "secondary", "danger", "warning"]; export const typeIcon = ["face", "checkbox", "doneOutline", "moreHoriz", "lock"];
render関数でIconコンポーネントを作る(実装編)
突然ですが、完成形です。(効果音)
// icon/Icon.js import { typeColor, typeIcon } from "./types"; import icon from "./icons"; export default { name: "Icon", props: { color: { type: String, default: "primary", validator: v => typeColor.includes(v) }, name: { type: String, default: "face", validator: v => typeIcon.includes(v) }, size: { type: Number, default: 24 } }, methods: { getIcon() { return icon[this.name] || icon.checkbox; } }, render(h) { const IconComponent = this.getIcon(); return h( "i", { class: "Icon" }, [ h(IconComponent, { attrs: { size: this.size, color: this.color } }) ] ); } };
templateもstyleも綺麗さっぱり無くなりました。
そして、例の暗黒物質も目に入ることが無くなりましたね。嬉しい!!!
解説
コンポーネント定義
h( "i", { class: "Icon" }, [ h(IconComponent, { attrs: { size: this.size, color: this.color } }) ] );
ついにhがでてきました。ここでは、実際にレンダリングされるコンポーネントとその属性やdirectionなどを定義していきます。
使用できるパラメータに関してはこちらを参照してください。
ちなみにtemplateを使った場合の比較は以下の通りです。
<template> <i class="Icon"> <IconComponent :size="size" :color="color" /> </i> </template>
Iconコンポーネントの取得
getIcon() { return icon[this.name] || icon.checkbox; }
コードの通りなのですが、主に暗黒物質を綺麗にした箇所がこちらになります。 アイコンリストを辞書化しているので、相応のアイコン名をkeyにとして各アイコンを取得することができます。
render関数では使用するcomponentsを定義する必要がありませんので、こういったシンプルな書き方ができるのです。
完成形
おまけ
せっかくIconコンポーネントを作成したので、もっと実用化してみましょう。
以下はIconコンポーネントを用いたTextFieldコンポーネントですが、 エラー時はアイコンごとカラーが変化するようになっています。みたいな。
// atoms/IconTextField.vue <template> <div class="IconTextField" :style="styles.root"> <Icon :color="attr.icon" :size="20" name="lock" class="IconTextField__icon" /> <input :id="`${name}-TextField`" :class="classes.input" :name="name" :value="value" :placeholder="placeholder" type="text" @input="onInput" /> </div> </template> <script> import Icon from "../icon/Icon"; export default { name: "IconTextField", components: { Icon }, props: { name: { type: String, default: "input" }, value: { type: String, default: "" }, placeholder: { type: String, default: "placeholder" }, fullWidth: { type: Boolean, default: false }, error: { type: Boolean, default: false } }, methods: { onInput(e) { if (e) { this.$emit("input", e.target.value); } } }, // Optional Chaining使いてぇ〜〜〜。 computed: { styles() { return { root: { width: this.fullWidth ? "100%" : null } }; }, classes() { return { input: this.error ? "IconTextField__input--error" : "IconTextField__input" }; }, attr() { return { icon: this.error ? "danger" : "secondary" }; } } }; </script>
完成形
おわり
render関数、使ってみたくなりましたか?
もしそう思っていただけると記事にした甲斐があるので嬉しいです。
フロントエンドの開発経験としてはまだまだ若いですが、これから定期的に発信させていただければと思います。最後までご覧いただきありがとうございました!
ご意見や改善点などありましたら、ぜひいただけると嬉しさの極みです。
今回のサンプルリポジトリ
投稿者について
ハローtetty活躍の場!
ご挨拶
どうも、tetty(ててぃ)です。 tettyの由来は本名+トッティ からです。どうでもいいですね。
さて、初のアドベントカレンダー参加をしたということもありますが、ブログを書く余裕もじわじわとできてきたので、これから色々と書き留めていきます。
また、当ブログは投稿者のモノづくり記録を主な目的として運営していきますが、訪問していただいた方にも何かインプットがあれば幸いです。
投稿者について
tettyです。 おそらく日本で最大規模の某専門学校に通っている21歳♂です。 ちなみに、某情報系国立大学の推薦を蹴った罪を背負って生きています。
やっていること
主にスタートアップベンチャー数社で色々やらせていただいてます。
- 外部CTO
- PM
- 立ち上げ開発
- パートナー開発(フロントエンド中心)などなど
Q > ...ちゃんと学校行ってる? A > 行ってます。でも最近はやばいです。
投稿者はクリエイティブであることに命を注いでいるので、スケジュールが詰まっていようが結果良ければ全て良しなのです。良いモノ作って天に召されたい。
もう少し詳しく!
投稿者の仕事場を並べておきます。
noFRAME schools, inc.
- 外部CTOやPMなどやってます(エンジニア募集中だよ)
https://www.noframeschools.com/www.noframeschools.com
株式会社プレックス
- フロントエンドの選定・構築・開発・コードレビューなど
Nzigen, Inc.
- 技術者としての基礎を叩き込んでいただいた。
- 主にフロントエンドのパートナー開発。
SNSもやってるよ
- wantedly(更新しばらくしてないので許して欲しい)
好み
終わりに
雰囲気はわかっていただけましたでしょうか? 自分のことについては、何かの節目で更新していこうと思っています。 ではでは、以後よろしくお願いいたします!