概要
何をやるか
-
モノレポ?みたいなYarnのWorkspaceを立てる
-
oaoを使って、配下のプロジェクト(
frontend
とbackend
)のdev
スクリプトを一度に走らせることができるようにする -
graphql-codegenで自動で型の定義ファイルを
frontend
に吐くようにする
-
oaoを使って、配下のプロジェクト(
-
バックエンド編
- graphql-yogaでGraphQLサーバーを立てる
- type-graphqlでGraphQLのリゾルバを書く
- コードファーストで、
schema.graphql
はサーバー起動時に吐くようにする - ts-node-devで、コードの更新があればバックエンドサーバーを再起動させる
-
フロントエンド編
今日作るやつ
できたやつ
ファイル構成
.
├── backend
│ ├── package.json
│ ├── index.ts
│ ├── resolvers
│ │ ├── AisatsuResolver.ts
│ │ └── CrabResolver.ts
│ ├── tsconfig.json
│ └── types
│ └── Crab.ts
├── frontend
│ ├── package.json
│ ├── lib
│ │ └── client.ts
│ ├── pages
│ │ └── index.tsx
│ ├── queries
│ │ ├── hello.ts
│ │ ├── kani.ts
│ │ └── types.ts
│ ├── store
│ │ └── pageController.ts
│ ├── .babelrc
│ └── tsconfig.json
├── package.json
├── codegen.yml
├── schema.graphql
├── tsconfig.json
└── yarn.lock
ルートプロジェクトの作成
yarn workspacesを設定する
yarn init
を行い、package.json
に以下を記載すればworkspace
となり、この配下で作ったnpm
プロジェクトでyarn install
すればルートプロジェクトのnode_modules
に引き上げられる。
{
"private": true,
"workspaces": [
"frontend",
"backend"
]
}
ルートにoao
をインストールする
ワークスペース上でyarn add
するには-W
オプションが必要になる。以下でoao
がインストールされる。
yarn add -D oao -W
oao
を使うと配下プロジェクトのスクリプトを一気に走らせることができる。またこの際に--parallel
をつけると並列で一気に走る。サーバーのようにexit
返さない奴を同時に走らせるときに限らずに早い。直列にする必要がなければ--parallel
つけておけばいいんじゃないかな。知らんけど。
"scripts": {
"dev": "oao run-script dev --parallel",
}
配下プロジェクトのnode_module
を全部消す
以下のコマンドで一気に消せる。
yarn oao clean
配下プロジェクトのpackage.json
を見てnode_module
をインストールする
yarn
のworkspaces
を設定しているなら、ルートでyarn
すれば入る。
yarn
ルートにtsconfig.json
の雛形を書く
Next.jsのGitHubに書かれているtsconfig.json
を引っ張ってきた。target
などは配下のプロジェクトでこれをextends
して設定する。
{
"compilerOptions": {
"allowJs": true /* Allow JavaScript files to be type checked. */,
"alwaysStrict": true /* Parse in strict mode. */,
"esModuleInterop": true /* matches compilation setting */,
"isolatedModules": true /* to match webpack loader */,
"noEmit": true /* Do not emit outputs. Makes sure tsc only does type checking. */,
/* Strict Type-Checking Options, optional, but recommended. */
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"strict": true /* Enable all strict type-checking options. */
}
}
バックエンド編
バックエンドのプロジェクトを作る。oao
はlernaのように、サブプロジェクト作るときに注意するべきことは特になさそうなので、普通にmkdir
なりして、そこでyarn
を叩けば良い。
mkdir backend
cd backend
必要なパッケージを入れる
yarn add graphql graphql-yoga type-graphql @types/graphql reflect-metadata uuid
yarn add -D @types/uuid ts-node-dev typescript
dev
ではない依存関係に@types/graphql
が入っていて気持ち悪いが、これでオッケー。
執筆当時のbackend/package.json
は以下のようになっていた。
{
"dependencies": {
"@types/graphql": "^14.2.2",
"graphql": "^14.4.2",
"graphql-yoga": "^1.18.0",
"reflect-metadata": "^0.1.13",
"type-graphql": "^0.17.4",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/uuid": "^3.4.5",
"ts-node-dev": "^1.0.0-pre.40",
"typescript": "^3.5.2"
}
}
ここにdev
スクリプトを設定しておく。
"scripts": {
"dev": "ts-node-dev index.ts "
},
tsconfig.json
をextends
して書く
ルートに書いたtsconfig.json
に足りないオプションや、変更が必要なオプションを書く。
{
"extends": "../tsconfig",
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"lib": ["es2016", "esnext.asynciterable"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
HelloWorld GraphQLを立てる
今回の例のHelloWorldなindex
を書いておく。
import "reflect-metadata";
import { GraphQLServer } from "graphql-yoga";
import { buildSchema, Query, Resolver } from "type-graphql";
@Resolver()
class HelloResolver {
@Query(() => String, {
description: "新しい言語を学ぶ当たって、Hello Worldをしないと呪われる。"
})
sayHello() {
return "Hello World!";
}
}
const main = async () => {
const schema = await buildSchema({
resolvers: [HelloResolver]
});
const server = new GraphQLServer({ schema });
server.start(
() => console.log("Server is running on localhost:4000")
);
};
main();
以上でyarn dev
を行うと4000番ポートにGraphQLサーバーが立つ。またts-node-dev
はソースの変更があれば、サーバを自動的に再起動してくれる。便利。
yarn dev
デフォルト設定ではgraphql-playgroundが立っているので、好き勝手遊べばいいといいたところですが、今はsayHello
しか立っていないないので面白みがない。
http://localhost:4000
type-graphql
とgraphql-yoga
で立てる時のポイント
-
type-graphql
で定義を書くときはimport "reflect-metadata";
をつける - IDEの補完を効かせるのに便利なスキーマは
buildSchema
にemitSchemaFile: path.resolve(__dirname, "../schema.graphql")
オプションを加えてルートに吐かせる。 - あとあと3000番ポートで
next.js
サーバーを立てるので、cors
オプションをserver.start
に食わせる。
import "reflect-metadata";
import { GraphQLServer } from "graphql-yoga";
import { buildSchema } from "type-graphql";
import { CrabResolver } from "./resolvers/CrabResolver";
import { AisatsuResolver } from "./resolvers/AisatsuResolver";
import path from "path";//追加
const main = async () => {
const schema = await buildSchema({
resolvers: [AisatsuResolver, CrabResolver],
emitSchemaFile: path.resolve(__dirname, "../schema.graphql")//追加
});
const server = new GraphQLServer({ schema });
server.start(
{
cors: {//追加
credentials: true,
origin: ["http://localhost:3000"]
}
},
() => console.log("Server is running on localhost:4000")
);
};
main();
今回書いたGraphQLサーバーの残りのコード
Type-GraphQL
の説明は次の機会があればやってみようと思う。デコレータをやたら付けたコードからスキーマが出力される形式なので、スキーマとtypescript
の型の二度付けがいらなくなってありがたい。とはいえ、クライアントサイドからこれらのクラスを使って、apollo-client
のクエリを発行するなり、戻り値の型を取得するなりができる感じじゃなかった。
しかし、リゾルバの書き心地が良かった。
import "reflect-metadata";
import { Field, ID, Int, ObjectType } from "type-graphql";
@ObjectType({ description: "かに" })
export class Crab {
@Field(() => ID)
id: string;
@Field({ description: "かにの名前" })
name: string;
@Field({ description: "かにの説明", nullable: true })
description: string;
@Field(() => Int, { description: "かにの値段" })
price: number;
@Field(() => [String], { description: "かにの失った脚", nullable: true })
lostLegs: string[];
constructor(data: {
id: string;
name: string;
description: string;
price: number;
lostLegs: string[];
}) {
this.id = data.id;
this.name = data.name;
this.description = data.description;
this.price = data.price;
this.lostLegs = data.lostLegs;
}
}
import { Query, Resolver } from "type-graphql";
import { Crab } from "../types/Crab";
import uuid from "uuid";
@Resolver(Crab)
export class CrabResolver {
private readonly kani: Crab[] = [
new Crab({
id: uuid.v4(),
name: "カニ太郎",
price: 50000,
description: "つよいカニ",
lostLegs: []
}),
new Crab({
id: uuid.v4(),
name: "カニ次郎",
price: 55000,
description: "よりつよいカニ",
lostLegs: ["左後ろ脚"]
})
];
@Query(() => Crab, { description: "かにの長男を呼ぶ" })
async getKani() {
return this.kani[0];
}
@Query(() => [Crab], { description: "かにの一族を呼ぶ" })
async getKanis() {
return this.kani;
}
}
フロントエンド編
フロントエンドのプロジェクトを作る。
cd..
mkdir frontend
cd frontend
必要なパッケージを入れる
yarn add apollo-boost graphql isomorphic-fetch mobx mobx-react-lite next react react-dom
yarn add -D @babel/core @types/graphql @types/node @types/react @types/react-dom babel-preset-mobx graphql-tag typescript
{
"dependencies": {
"apollo-boost": "^0.4.3",
"graphql": "^14.4.2",
"isomorphic-fetch": "^2.2.1",
"mobx": "^5.11.0",
"mobx-react-lite": "^1.4.1",
"next": "^8.1.1-canary.69",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
"devDependencies": {
"@babel/core": "^7.5.0",
"@types/graphql": "^14.2.2",
"@types/node": "^12.0.12",
"@types/react": "^16.8.23",
"@types/react-dom": "^16.8.4",
"babel-preset-mobx": "^2.0.0",
"graphql-tag": "^2.10.1",
"typescript": "^3.5.2"
}
}
oao
で一気にdev
が走ると嬉しいので、書くまでもないがdev
スクリプトを用意しておく。
"scripts": {
"dev": "yarn next"
},
tsconfig.json
をextends
して書く
next.js
を使うため、"extends": "../tsconfig"
と"experimentalDecorators": true,
さえ入れておけば一度yarn next
すればtsconfig.json
を勝手に完成させてくれる。なお出力はこうなった。
{
"extends": "../tsconfig",
"compilerOptions": {
"experimentalDecorators": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"jsx": "preserve"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
MobX
を使いたいので.babelrc
をちょびっと書く
{
"presets": ["next/babel", "mobx"]
}
型の自動生成
使いそうなGraphQL
クエリを書く
queries
フォルダ配下に使いそうなクエリを書いてエクスポートしておく。なお、query hogehoge
部分は省略できるが、次の型の自動生成の時の名前になるのであえてつける。決まり切った書き方するので、IDEのライブテンプレートなりを作っておくと気持ちよくなる。
import gql from "graphql-tag";
export const getHello = gql`
query getHello {
sayHello
}
`;
export const getHelloKani = gql`
query getHelloKani {
sayHello
sayKani
}
`;
import gql from "graphql-tag";
export const getKaniSay = gql`
query getKaniSay {
sayKani
}
`;
export const getKani = gql`
query getKani {
getKani {
name
}
}
`;
graphql-codegen
をルートにインストールする
ルートディレクトリにgraphql-codegen
とその親戚を入れる。
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations graphql -W
graphql-codegen
の設定を書く
型生成の設定ファイルを書く。設定ファイルほど書きたくないものは世になし。
overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。サーバが立ってさえ入れば勝手にスキーマを読み込んでくれる
documents: "./frontend/queries/*.ts" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
./frontend/queries/types.ts: # 出力先
plugins:
- typescript # 基本型とtypeの型を吐く
- typescript-operations # クライアントからそのクエリを投げた時の戻り値型を作成してくれる
凝ったことをしたければドキュメントを読みましょう。
graphql-codegen
のウォッチャを走らせる
codegen
なるスクリプトを定義してこれを走らせる。 --watch
オプションをつけておけば、更新に応じて自動で型をどんどん/frontend/queries/types.ts
に吐いてくれる。
"scripts": {
"dev": "oao run-script dev --parallel",
"codegen": "graphql-codegen --config codegen.yml --watch"
},
yarn codegen
生成される型
使い道としては、戻り値の型。
export 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;
};
/** かに */
export type Crab = {
__typename?: "Crab";
id: Scalars["ID"];
/** かにの名前 */
name: Scalars["String"];
/** かにの説明 */
description?: Maybe<Scalars["String"]>;
/** かにの説明 */
price: Scalars["Int"];
/** かにの失った脚 */
lostLegs?: Maybe<Array<Scalars["String"]>>;
};
export type Query = {
__typename?: "Query";
/** かにの長男を呼ぶ */
getKani: Crab;
/** かにの一族を呼ぶ */
getKanis: Array<Crab>;
/** あいさつは大事 */
sayHello: Scalars["String"];
/** かにもあいさつする */
sayKani: Scalars["String"];
};
export type GetHelloQueryVariables = {};
export type GetHelloQuery = { __typename?: "Query" } & Pick<Query, "sayHello">;
export type GetHelloKaniQueryVariables = {};
export type GetHelloKaniQuery = { __typename?: "Query" } & Pick<
Query,
"sayHello" | "sayKani"
>;
export type GetKaniSayQueryVariables = {};
export type GetKaniSayQuery = { __typename?: "Query" } & Pick<Query, "sayKani">;
export type GetKaniQueryVariables = {};
export type GetKaniQuery = { __typename?: "Query" } & {
getKani: { __typename?: "Crab" } & Pick<Crab, "name">;
};
クライアントを書く
ApolloClient from "apollo-boost"
はサーバー側とクライアント側でキャッシュ先とかブラウザのfetch
とかNode
のfetch
なんか使っているのが違うっぽいので、よくわからん怒られが発生する。特に循環参照とかよくわらんのがでやがるので、多分これでうまくいくだろうというやり方を書いた。apollo-boost
ではないapollo-client
のApolloClient
を使うと楽に書けない代わりに、そこらへんをはっきりできていいと思う。知らんけど。
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";
export class Client {
static readonly uri = "http://localhost:4000";
//サーバーなら呼んでも問題ないクライアント
private static serverCLient = new ApolloClient({
uri: Client.uri
});
//クライアントで呼ばれる日には安心なクライアント?
private static clientClient: ApolloClient<any> | null;
public static get client(): ApolloClient<any> {
if (!process.browser) {
return this.serverCLient;
}
if (!this.clientClient) {
this.clientClient = new ApolloClient({ uri: Client.uri });
}
return this.clientClient;
}
}
ストアを書く
import * as QueryType from "../queries/types";
が自動生成される戻り値の型。これのおかげでIDEの補完が捗りよろしい。
import { observable } from "mobx";
import { Client } from "../lib/client";
import { getHello } from "../queries/hello";
import * as kaniQuery from "../queries/kani";
import * as QueryType from "../queries/types";
export interface PageModel {
text: string;
}
export class PageController implements PageModel {
@observable text: string = "";
constructor(model: PageModel = { text: "" }) {
this.initialize(model);
}
initialize(model: PageModel) {
this.text = model.text;
}
async fetchKani() {
const res = await Client.client.query<QueryType.GetKaniQuery>({
query: kaniQuery.getKani
});
this.text = res.data.getKani.name;
}
async fetchKaniSay() {
const res = await Client.client.query<QueryType.GetKaniSayQuery>({
query: kaniQuery.getKaniSay
});
this.text = res.data.sayKani;
}
async fetchHello() {
const res = await Client.client.query<QueryType.GetHelloQuery>({
query: getHello
});
this.text = res.data.sayHello;
}
}
ページを書く
import { NextPage } from "next";
import { Observer } from "mobx-react-lite/";
import { PageController, PageModel } from "../store/pageController";
import { useState } from "react";
const Index: NextPage<{ model: PageModel }> = props => {
const [controller] = useState(new PageController(props.model));
return (
<div>
<button
onClick={() => {
controller.fetchHello();
}}
>
へろー
</button>
<button
onClick={() => {
controller.fetchKani();
}}
>
カニ
</button>
<Observer>{() => <p>{controller.text}</p>}</Observer>
</div>
);
};
Index.getInitialProps = async () => {
const controller = new PageController();
await controller.fetchKaniSay();
return { model: controller };
};
export default Index;
NextのgetInitialProps
は簡単なオブジェクトしか渡せないので、ここら辺の対応を工夫する必要がある。
フロントサーバを立ち上げる
先ほどのGraphQLサーバが立っているならば、frontendでyarn dev
を叩けばNextが立つ。
yarn dev
もし、先ほどのGraphQLサーバーを落としているなら、ルートでyarn dev
をすれば、冒頭のように起動する。