在 ts 中使用 graphql

本文最后更新于:2022年3月28日 凌晨

场景

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

结合 ts 使用

基本思路

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

使用步骤

以下使用 github api@v4 进行演示

获取后端的元数据

1
2
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

1
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的内容。

1
2
3
4
5
6
7
8
9
10
11
fragment Repo on Repository {
id
name
}

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

使用 cli 生成类型定义

1
pnpm graphql-codegen --config codegen.yml

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// 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>

我们可以这样使用它

1
2
3
4
5
6
7
8
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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "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

    1
    2
    3
    4
    5
    6
    7
    8
    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 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 尝试多线程加速

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 配置文件(当然仍然可以自行维护),使用方法

安装依赖

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

配置插件

1
2
3
4
5
6
7
8
9
10
// 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 查询对象

在 ts 中使用 graphql
https://blog.rxliuli.com/p/349ef4aeec0c466c8566d8383f596941/
作者
rxliuli
发布于
2021年10月26日
许可协议