用 graphql-codegen 生成类型定义 (React, Apollo, TypeScript)

这篇文章总结了在这个仓库中所做的事情。

对于 GraphQL + TypeScript 的挑战感

使用TypeScript(以及其他静态类型语言)和GraphQL时,往往会出现类型的重复定义问题。尽管我们花了很多心思在GraphQL上编写通信协议的类型,但通过多重定义这些类型可能会增加运维的繁琐性并可能成为错误的温床,这是一个令人担忧的问题。

本次旨在编写GraphQL的模式和查询,为服务器生成解析器的类型定义,为客户端生成查询的类型定义,以便尽可能地处理类型安全的代码。

做法

我们会使用 graphql-code-generator(主要是介绍这个库)。

期待中的目录结构就像这样


├── graphql
│   ├── mutations
│   │   ├── addUser.graphql
│   │   └── deleteUser.graphql
│   ├── queries
│   │   ├── user.graphql
│   │   └── users.graphql
│   └── schema.graphql
├── client
│   ├── index.html
│   └── main.tsx
├── server
│   ├── index.ts
│   ├── package.json
│   └── resolvers.ts
└── codegen.yml

虽然有点凌乱,但是

GraphQL 通过定义模式
客户端 实现客户端
服务器 实现GraphQL服务器

是的。

生成数据模型

假设我们在 server/gen 目录中定义了 GraphQL 解析器的类型,并在 client/gen 目录中生成了用于 GraphQL 查询和变更的 hooks API。

这个软件包有很多零散的部分,依赖项也相当多,但我们会安装这些组件。

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

我写了一个类似以下的 codegen.yml,尽管可以用 yarn graphql-codegen init 来生成。

overwrite: true
schema:
  - ./graphql/schema.graphql
documents:
  - ./graphql/queries/*.graphql
  - ./graphql/mutations/*.graphql
generates:
  ./server/gen/graphql-resolver-types.ts:
    plugins:
      - typescript
      - typescript-resolvers
  ./client/gen/graphql-client-api.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withComponent: false
      withHooks: true
      withHOC: false
  ./graphql/schema.json:
    plugins:
      - introspection
    • typescript-resolver を使ったリゾルバ用型定義

 

    • typescript-react-apollo を使った React 向け型定義

 

    schema.json

我打算吐出来。

通过 Yarn 执行命令来生成代码。

$ yarn graphql-codegen --config codegen.yml
  ✔ Parse configuration
  ✔ Generate outputs

我們將在使用過程中進行生成代碼的確認。

服务器端解析器的类型定义

这是一个使用typeorm的简单CRUD示例。(其实坦率地说,这个仓库是用来练习typeorm的)

通过生成的类型定义,我们可以获得MutationResolvers和QueryResolvers,从而可以在解析器的实现中为其添加类型。

import { ulid } from 'ulid';
import {
  MutationResolvers,
  QueryResolvers,
  Resolvers,
} from './gen/graphql-resolver-types';
import { User } from './entity/User';

const Query: QueryResolvers = {
  async user(_parent, args, _context, _info) {
    const user = await User.findOne({ id: args.id });
    return user || null;
  },
  async users() {
    const users = await User.find();
    return users;
  },
};

const Mutation: MutationResolvers = {
  async addUser(_parent, args, _context, _info) {
    const newUser = new User();
    newUser.id = ulid();
    newUser.name = args.name;
    await User.save(newUser);
    return newUser;
  },
  async deleteUser(_parent, args, _context, _info) {
    const user = await User.findOne(args.id);
    await User.delete(args.id);
    return user;
  },
};

export const resolvers: Resolvers = {
  Query,
  Mutation,
};

只要使用VSCode等软件进行操作就能明白,第二个参数args需要指定类型,并且返回值也需要指定类型。

从查看生成的代码,可以看到以下代码生成。

//...

export type MutationResolvers<Context = any, ParentType = Mutation> = {
  addUser?: Resolver<User, ParentType, Context, MutationAddUserArgs>,
  deleteUser?: Resolver<User, ParentType, Context, MutationDeleteUserArgs>,
};

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

现在可以放心地撰写解决方案了。

使用客户端生成的代码

假设您已经在 http://localhost:3333 上运行了一个通过↑创建的 GraphQL 服务器。
各种设置已被省略,请参阅存储库。为了简化,我们使用了parcel。

请关注如何使用生成的代码,特别是将`import … from ‘./gen/graphql-client-api’`代码和`UserList`的钩子函数配合使用。

import React from 'react';
import ReactDOM from 'react-dom';
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import {
  useUsersQuery,
  useAddUserMutation,
  useDeleteUserMutation,
} from './gen/graphql-client-api';

const client = new ApolloClient({
  link: createHttpLink({
    uri: 'http://localhost:3333/graphql',
  }),
  cache: new InMemoryCache(),
});


function UserList() {
  const usersQuery = useUsersQuery();
  const addUserMutation = useAddUserMutation();
  const deleteUserMutation = useDeleteUserMutation();

  return (
    <>
      <h1>Users</h1>
      <ul>
        {!usersQuery.loading &&
          usersQuery.data.users.map(user => {
            return (
              <li key={user.id}>
                {user.id}:{user.name}
                <button
                  onClick={async () => {
                    await deleteUserMutation({ variables: { id: user.id } });
                    usersQuery.refetch();
                  }}
                >
                  delete
                </button>
              </li>
            );
          })}
      </ul>
      <button
        onClick={async () => {
          await addUserMutation({
            variables: { name: Math.random().toString() },
          });
          usersQuery.refetch();
        }}
      >
        addUser
      </button>
    </>
  );
}

function App() {
  return (
    <ApolloProvider client={client}>
      <ApolloHooksProvider client={client}>
        <UserList />
      </ApolloHooksProvider>
    </ApolloProvider>
  );
}

ReactDOM.render(<App />, document.querySelector('.root'));

如果出现副作用,用户应该调用usersQuery.refetch()。

印象

通过从模式生成代码,我能够最小化自己定义的部分,并且能够有效地进行TypeScript的类型检查。

通过使用GraphQL和类型,我感觉到整体上不再那么浪费,这使得整个过程变得更加轻松,我终于有了投入生产环境的动力。

在严格意义上,typeorm和GraphQL之间仍然存在一些类似重复定义的部分,但考虑到GraphQL并不是ORM的特点,我认为保持它们分离是更好的选择。

如果想将GraphQL用作ORM,使用Prisma会更方便。

Prisma – 最快的 GraphQL 服务器实现 – Qiita

广告
将在 10 秒后关闭
bannerAds