搭建好Next.js服务器和GraphQL服务器,愉快地用TypeScript练习GraphQL开发技巧

简要概述

你要做什么

    1. 建立一个类似于Yarn Workspace的Monorepo项目。

使用oao,使得可以同时运行frontend和backend项目的dev脚本。

使用graphql-codegen自动将类型定义文件生成到frontend项目中。

后端编辑:

使用graphql-yoga搭建GraphQL服务器。

使用type-graphql编写GraphQL解析器。

通过代码优化,在服务器启动时自动生成schema.graphql文件。

使用ts-node-dev,在代码更新时重新启动后端服务器。

前端编辑:

使用Next.js和mobx进行舒适的编码。

编写查询,并自动由graphql-codegen生成返回类型。

今天要做的东西

画面収録 2019-07-07 1.48.08-2.gif

可以的那个

文件结构

.
├── 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工作区

使用 `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的工作空间,只需要在根目录下运行yarn即可进入。

yarn

在根目录下编写tsconfig.json的模板

从 Next.js 的 GitHub 上拉取了 tsconfig.json 文件。可以在项目的子目录中使用 extends 来配置 target 等设置。

{
  "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

尽管有一个不是开发环境的依赖@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文件。

在根目录下的tsconfig.json中补充缺少或需要更改的选项。

{
  "extends": "../tsconfig",
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": ["es2016", "esnext.asynciterable"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

你好世界,我要建立GraphQL。

请写下这个例子中的HelloWorld索引。

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后,GraphQL服务器将在4000端口上启动。另外,ts-node-dev会在源代码更改时自动重新启动服务器。非常方便。

yarn dev

默认设置下,graphql-playground已经启动了,所以你可以随心所欲地玩耍。但是现在只有sayHello在运行,所以没有趣味。
http://localhost:4000

スクリーンショット 2019-07-07 2.36.41.png

使用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类型的形式,所以不再需要模式和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脚本,虽然不用写。

"scripts": {
    "dev": "yarn next"
},

使用extends语句编写tsconfig.json文件。

只要使用了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">;
};

写客户

“apollo-boost”的ApolloClient在服务器端和客户端使用不同的缓存和浏览器的fetch和Node的fetch之类的东西,所以很容易引发不明所以的错误。特别是循环引用之类的我也不太懂,所以我写了一种我觉得可能行得通的方法。相比于使用apollo-boost中的ApolloClient,使用不是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;
  }
}

写商店 (xiě

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服务器已经启动了,只要在前端运行 `yarn dev`,Next就会启动。

yarn dev

如果你刚才关闭了GraphQL服务器,则可以在根目录下运行yarn dev来启动,就像开头一样。

bannerAds