在 ts 中使用 graphql

本文最后更新于:2022年3月28日 上午

场景

graphql 提供前后端一致的 api 架构元数据,同时通过请求合并、按需获取加快 web 与后端交互的性能。

结合 ts 使用

基本思路

  1. 扫描 gql 文件中的查询文件
  2. 生成类型定义与 document 对象
  3. 使用这些类型定义

使用步骤

以下使用 github [email protected] 进行演示

获取后端的元数据

pnpm i -g get-graphql-schema
get-graphql-schema https://docs.github.com/public/schema.docs.graphql > schema.graphql

参考:https://stackoverflow.com/questions/37397886/get-graphql-whole-schema-query

安装基础 sdk

pnpm i @apollo/client graphql

安装代码生成器相关依赖

  • @graphql-codegen/cli 基础 cli
  • @graphql-codegen/typescript ts 插件
  • @graphql-codegen/typescript-operations ts 操作生成插件
  • @graphql-codegen/typed-document-node 生成 document 对象
  • @graphql-codegen/near-operation-file-preset ts 预设配置

创建配置 codegen.yml

overwrite: true
schema: schema.graphql
generates:
  ./src/graphql.generated.ts:
    plugins:
      - typescript
  ./:
    documents:
      - src/**/*.gql
    preset: near-operation-file
    presetConfig:
      baseTypesPath: ./src/graphql.generated.ts
      extension: .generated.ts
    plugins:
      - typescript-operations
      - typed-document-node

src/api/RepoQuery.gql 编写 graphql 查询语句

注: 在非 react 项目中,请从 @apollo/client/core 导入所有非 react 的内容。

fragment Repo on Repository {
  id
  name
}

query findRepoStar($name: String!, $owner: String!) {
  repository(name: $name, owner: $owner) {
    ...Repo
    stargazerCount
  }
}

使用 cli 生成类型定义

pnpm graphql-codegen --config codegen.yml

生成结果大致如下,基本上就是参数和结果类型

// src/api/RepoQuery.generated.ts
import * as Types from "../graphql.generated";

import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
export type RepoFragment = {
  __typename?: "Repository";
  id: string;
  name: string;
};

export type FindRepoStarQueryVariables = Types.Exact<{
  name: Types.Scalars["String"];
  owner: Types.Scalars["String"];
}>;

export type FindRepoStarQuery = {
  __typename?: "Query";
  repository?:
    | {
        __typename?: "Repository";
        stargazerCount: number;
        id: string;
        name: string;
      }
    | null
    | undefined;
};

export const RepoFragmentDoc = {
  kind: "Document",
  definitions: [
    {
      kind: "FragmentDefinition",
      name: { kind: "Name", value: "Repo" },
      typeCondition: {
        kind: "NamedType",
        name: { kind: "Name", value: "Repository" },
      },
      selectionSet: {
        kind: "SelectionSet",
        selections: [
          { kind: "Field", name: { kind: "Name", value: "id" } },
          { kind: "Field", name: { kind: "Name", value: "name" } },
        ],
      },
    },
  ],
} as unknown as DocumentNode<RepoFragment, unknown>;
export const FindRepoStarDocument = {
  kind: "Document",
  definitions: [
    {
      kind: "OperationDefinition",
      operation: "query",
      name: { kind: "Name", value: "findRepoStar" },
      variableDefinitions: [
        {
          kind: "VariableDefinition",
          variable: { kind: "Variable", name: { kind: "Name", value: "name" } },
          type: {
            kind: "NonNullType",
            type: {
              kind: "NamedType",
              name: { kind: "Name", value: "String" },
            },
          },
        },
        {
          kind: "VariableDefinition",
          variable: {
            kind: "Variable",
            name: { kind: "Name", value: "owner" },
          },
          type: {
            kind: "NonNullType",
            type: {
              kind: "NamedType",
              name: { kind: "Name", value: "String" },
            },
          },
        },
      ],
      selectionSet: {
        kind: "SelectionSet",
        selections: [
          {
            kind: "Field",
            name: { kind: "Name", value: "repository" },
            arguments: [
              {
                kind: "Argument",
                name: { kind: "Name", value: "name" },
                value: {
                  kind: "Variable",
                  name: { kind: "Name", value: "name" },
                },
              },
              {
                kind: "Argument",
                name: { kind: "Name", value: "owner" },
                value: {
                  kind: "Variable",
                  name: { kind: "Name", value: "owner" },
                },
              },
            ],
            selectionSet: {
              kind: "SelectionSet",
              selections: [
                {
                  kind: "FragmentSpread",
                  name: { kind: "Name", value: "Repo" },
                },
                {
                  kind: "Field",
                  name: { kind: "Name", value: "stargazerCount" },
                },
              ],
            },
          },
        ],
      },
    },
    ...RepoFragmentDoc.definitions,
  ],
} as unknown as DocumentNode<FindRepoStarQuery, FindRepoStarQueryVariables>;

我们可以这样使用它

const res = await client.query({
  query: FindRepoStarDocument,
  variables: {
    name: "liuli-tools",
    owner: "rxliuli",
  },
});
console.log("res: ", res.data.repository?.stargazerCount);

Jetbrains IDE 支持

  1. 安装插件 JS GraphQL

  2. 创建 graphql 基础配置文件 .graphqlconfig

    {
      "name": "github GraphQL Schema",
      "schemaPath": "schema.graphql",
      "extensions": {
        "endpoints": {
          "Default GraphQL Endpoint": {
            "url": "https://api.github.com/graphql",
            "headers": {
              "user-agent": "JS GraphQL",
              "Authorization": "bearer ${env:GH_TOKEN}"
            },
            "introspect": false
          }
        }
      }
    }
  3. 最终效果
    1635317727128

VSCode 支持

  1. 安装插件 GraphQL

  2. 在根目录添加配置 .graphqlconfig

    module.exports = {
      client: {
        service: {
          name: "github GraphQL Schema",
          localSchemaFile: "./schema.graphql",
        },
      },
    };
  3. 最终效果
    1635316734084

注:在 monorepo 中,vscode 插件仅支持在根目录添加配置文件,所以其它子目录中的配置仅用于与生态中的其他工具结合。
graphql 插件不支持查询参数的体验,只有一个非常简单的输入框

集成到 vite

关于为什么要将自动生成作为 vite 插件集成,之前在 是否需要将 cli 工具集成到构建工具中 中已经说明了。

vite-plugin-graphql-codegen 监视模式实际上有 bug,所以自行维护一个 rollup 插件

import { Plugin } from "vite";
import { CodegenContext, generate, loadContext } from "@graphql-codegen/cli";

async function codegen(watch: boolean) {
  const codegenContext = await loadContext();
  codegenContext.updateConfig({ watch });
  try {
    await generate(codegenContext);
  } catch (error) {
    console.log("Something went wrong.");
  }
}

export function graphQLCodegen(): Plugin {
  let codegenContext: CodegenContext;

  return {
    name: "rollup-plugin-graphql-codegen",
    async buildStart() {
      // noinspection ES6MissingAwait
      codegen(this.meta.watchMode);
    },
  };
}

这里其实还可以使用 worker_threads 尝试多线程加速

import { Plugin } from "vite";
import { generate, loadContext } from "@graphql-codegen/cli";
import { isMainThread, parentPort, Worker } from "worker_threads";
import { expose, wrap } from "comlink";
import nodeEndpoint from "comlink/dist/umd/node-adapter";

async function codegen(watch: boolean) {
  const codegenContext = await loadContext();
  codegenContext.updateConfig({ watch });
  try {
    await generate(codegenContext);
  } catch (error) {
    console.log("Something went wrong.");
  }
}

if (!isMainThread) {
  expose(codegen, nodeEndpoint(parentPort!));
}

export function graphQLCodegen(): Plugin {
  return {
    name: "rollup-plugin-graphql-codegen",
    async buildStart() {
      const worker = new Worker(__filename);
      try {
        const codegenWorker = wrap<(watch: boolean) => void>(
          nodeEndpoint(worker)
        );
        // noinspection ES6MissingAwait
        codegenWorker(this.meta.watchMode);
      } finally {
        worker.unref();
      }
    },
  };
}

已发布至 @liuli-util/rollup-plugin-graphql-codegen


更新,默认包含 @graphql-codegen/cli,不再需要单独维护 codegen.yml 配置文件(当然仍然可以自行维护),使用方法

安装依赖

pnpm i -D @liuli-util/rollup-plugin-graphql-codegen @graphql-typed-document-node/core

配置插件

// vite.config.ts
import { defineConfig } from "vite";
import {
  gql2TsConfig,
  graphQLCodegen,
} from "@liuli-util/rollup-plugin-graphql-codegen";

export default defineConfig({
  plugins: [graphQLCodegen(gql2TsConfig)],
});

安装 chrome 插件

GraphQL Network Inspector

1637290440947

参考

其他方案?

  • 使用 rollup 插件 @rollup/plugin-graphql 直接将 .graphql 文件视为可导入的 es 模块 – 主要问题是要处理相关工具的兼容性,主要包括 jest/eslint。
  • 将 graphql 转成复杂的类型,然后运行时将 js 对象转换为 graphql 查询对象