本文于2023年1月24日星期二发表,Eddy Nguyen @ The Guild Blog
GraphQl Server通常是一个中心系统,团队需要能够开发功能,而
不阻止其他团队。每个团队都可以拥有各种标准和实践。因此,如果GraphQl
无法在结构中设置服务器,以允许并发贡献,开发减慢和
在管理任务上花费了更多时间,而不是提供新功能。
本博客文章探讨了一些常见问题GraphQL服务器按比例遇到的
推荐如何解决它们。
简单问题:代码所有权
问题:如何管理代码所有权?
在许多没有明确结构和准则的团队中共享一个代码库是
的食谱
灾难。代码库中的第一团队通常建立适合他们的结构。当一个
第二队加入,他们很可能遵循已经在那里的结构。同样的故事发生在
此后的每个团队。经过几回合,开发减慢了。可以回顾结构
并发现自己盯着一个由一个团队组成的代码库。
团队希望将其成员通知与他们的域相关的更改,但并非每个拉力
请求(PR)。如果您曾经通知您与团队无关的变化,则很可能是代码库
需要设置以支持许多从事它的团队。
在下面的示例中,A团队是第一个设置服务器的团队。他们管理User
和Auth
域,因此它们为这些文件创建datasources
和resolvers
文件夹:
├── src/
│ ├── schema/
│ │ ├── datasources/
│ │ │ ├── UserDatasource.ts
│ │ │ ├── AuthDatasource.ts
│ │ ├── resolvers/
│ │ │ ├── userResolvers.ts
│ │ │ ├── authResolvers.ts
│ │ ├── userSchema.graphql
│ │ ├── authSchema.graphql
│ ├── server.ts
│ ├── codegen.yml
然后,B团队进入代码库。他们管理Book
域,因此他们按照
添加文件
相同的结构:
├── src/
│ ├── schema/
│ │ ├── datasources/
│ │ │ ├── UserDatasource.ts
│ │ │ ├── AuthDatasource.ts
│ │ │ ├── BookDatasource.ts
│ │ ├── resolvers/
│ │ │ ├── userResolvers.ts
│ │ │ ├── authResolvers.ts
│ │ │ ├── bookResolvers.ts
│ │ ├── userSchema.graphql
│ │ ├── authSchema.graphql
│ │ ├── bookSchema.graphql
│ ├── server.ts
│ ├── codegen.yml
虽然这很小,但通信开销较低,但无法扩展。当有
数十个或数百个数据源和解析器,很难知道谁拥有什么。一个简单的
解决方案是使用GitHub的Colepter或类似功能将文件分配给所有者。但是它必须
在每个文件上都可以完成,因为文件分为类别文件夹,例如resolvers
,
datasources
等
很容易将此结构分为文件夹,每个结构都以管理它的团队命名:
├── src/
│ ├── schema/
│ │ ├── TeamA/ # Team A notified (by CODEOWNERS) if changes happen in this folder
│ │ │ ├── datasources/
│ │ │ │ ├── UserDatasource.ts
│ │ │ │ ├── AuthDatasource.ts
│ │ │ ├── resolvers/
│ │ │ │ ├── userResolvers.ts
│ │ │ │ ├── authResolvers.ts
│ │ │ ├── userSchema.graphql
│ │ │ ├── authSchema.graphql
│ │ ├── TeamB/ # Team B notified (by CODEOWNERS) if changes happen in this folder
│ │ │ ├── datasources/
│ │ │ │ ├── BookDatasource.ts
│ │ │ ├── resolvers/
│ │ │ │ ├── bookResolvers.ts
│ │ │ ├── bookSchema.graphql
│ ├── server.ts
│ ├── codegen.yml
由于所有权的定义,这已经是一个重大改进,但是组织问题
保持。例如,当团队A更改自己拥有的东西,分为较小的团队或
时会发生什么情况
更改名称?所有这些方案都需要管理工作:在最佳情况下重命名文件夹,
在最坏的情况下移动文件。这样的管理工作对最终用户没有任何价值。
最重要的是,所有数据源和解析器都必须放在GraphQL Server中。是
此示例中的server.ts
文件。谁拥有此文件和其他服务器维护,例如
软件包更新,安全补丁,codegen.yml
等。?
解决方案:分为模块
通常看到团队更改他们的名字,分为团队的规模或结合结构和
优先级变化。但是,长期以来通常保持稳定的是业务
领域。如果我们根据业务领域分配模式并相应地分配团队,则将变成
更可扩展。如果一个团队需要将一个域名交给另一个团队,他们只需要更新
Covelowner的文件。
最好让一小部分专用维护器用于服务器维护。这可以组成
代码库中每个团队的一个成员的成员可以旋转。或者,有些
公司可以选择将此责任分配给专门的团队。有一个专门的小组 -
让我们称他们为维护者 - 有助于减少其余团队的噪音和认知负荷,
允许他们专注于提供功能。
以前使用相同的示例,我们可以识别3个主要域:User
,Auth
和Book
,每个
设置了计量人员,以通知适当的更改团队。维护者拥有server.ts
和
其他配置等codegen.yml
(我们都使用
GraphQL Codegen,对吗? ð)。
├── src/
│ ├── schema/
│ │ ├── user/ # Team A notified if changed
│ │ │ ├── datasources.ts
│ │ │ ├── resolvers.ts
│ │ │ ├── schema.graphql
│ │ ├── auth/ # Team A notified if changed
│ │ │ ├── datasources.ts
│ │ │ ├── resolvers.ts
│ │ │ ├── schema.graphql
│ │ ├── book/ # Team B notified if changed
│ │ │ ├── datasources.ts
│ │ │ ├── resolvers.ts
│ │ │ ├── schema.graphql
│ ├── server.ts # Maintainers notified if changed
│ ├── codegen.yml # Maintainers notified if changed
棘手的问题:大规模的最佳实践对齐
将模式分为模块通常很容易让团队达成共识。然后我们开始看到
严重问题:如何为团队执行最佳实践,同时减少维护者的需求
花在服务器维护上。
1.如何为所有团队执行最佳实践
在澳大利亚炎热的夏日,不良的做法和惯例像丛林大火一样传播;这是
最糟糕的!我曾经是维护者团队的一员。我们有有关各种主题的准则,一个是
解析器命名公约。有一次,开发人员错误地使用了帕斯卡案,而不是
骆驼香烟盒。第二天,我在帕斯卡(Pascal)案中醒来了一半以上的解析器。 ð±
好吧,我在这里夸大了,但是经历非常创伤。
只有在人们关注它们的情况下,准则才是好的。标准开始滑倒而没有明确的和
自动执法和不良实践开始传播。
我们需要自动化工具来有效执行指南。幸运的是,这些天我们有广泛的
帮助GraphQL Server最佳实践的工具范围:
- GraphQL Codegen with typescript和 typescript-resolvers 用于TypeScript中的类型安全GraphQl Server开发的插件
- GraphQL ESLint用于验证,覆盖和检查 为了获得最佳实践和惯例(就像所有解析器一样,必须是骆驼盒!!!。
但是,有一些需要改进的领域。
例如,我正在处理的GraphQL服务器之一的指南是使用生成的类型
来自GraphQl Codegen。一些团队成员,尤其是代码库的新团队成员,可能尚未是
意识到这一点。幸运的是,其他团队成员或维护者在公关审查时间遇到了这些问题。
但是,如果我们有自动执行此准则的工具,这将节省所有人的时间和精力。
2.如何最大程度地减少对维护者的噪音
维护者通常需要管理GraphQl Server的这些方面:
- 核心服务器逻辑:解析器地图,架构,CI/CD等。
- 配置文件:
.graphqlrc
,codegen.yml
等。
更改维护者域中的任何内容都应通知它们。不幸的是,这发生了
定期进行常规工作流程。维护者还拥有团队的工作和一般计划和
安全更新以担心。因此,被通知无关的PR会很快导致倦怠。
例如,如果添加了新的解析器,则必须手动将其添加到解析器地图中。所以,
维护者会收到每个新的解析器的通知。如果您正在使用
,可能看起来像这样
GraphQL Yoga:
// server.ts (managed by the Maintainers)
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { Auth } from "./schema/auth/resolvers";
import { book, Book } from "./schema/auth/resolvers";
import { user, User } from "./schema/user/resolvers";
const schema = createSchema({
typeDefs: `...`,
resolvers: { // This is the resolver map
Query: {
user,
book,
},
Auth,
Book
User,
}
});
const yoga = createYoga({ schema });
const server = createServer(yoga)
server.listen(4000, () => {
console.info('Server is running on http://localhost:4000/graphql')
})
有一种减轻此问题的方法:每个模式模块导出一个解析器的对象,它们
作为数组传递到resolvers
。在内部,解析器使用
合并
mergeResolvers from @graphql-tools/merge.
这意味着在每个新模块而不是每个新的解析器上都会通知维护者:
// server.ts
import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import * as authResolvers from './schema/auth/resolvers'
import * as bookResolvers from './schema/book/resolvers'
import * as userResolvers from './schema/user/resolvers'
const schema = createSchema({
typeDefs: `...`,
resolvers: [
// mergeResolvers are called internally
authResolvers,
bookResolvers,
userResolvers
]
})
// rest of server config
但是,mergeResolvers
有一个警告:它只是在运行时合并普通的JavaScript对象。
因此,有人会意外地覆盖他人的解析器的风险。这个问题很难
在具有数百个解析器和模块的大型代码库中找到。
另一个常用的功能是
mappers.
此功能允许解析器返回自定义映射器对象,而不是GraphQL输出类型。
映射器的问题是,我们需要每次需要创建一个:
时都需要更新codegen.yml
。
# codegen.yml
generates:
src/schema/types.generated.ts:
plugins:
- typescript
- typescript-resolvers
mappers:
User: './mappers#UserMapper'
Profile: './mappers#ProfileMapper'
# Add another line for each mapper
这对维护人员来说是一个问题,因为这些更改是基于团队要求。是否
团队是否使用映射者是团队的选择,不应关心维护者。但是,
通知维护者,因为他们拥有codegen.yml
文件。
解决方案:使用GraphQL Server Codegen预设
要解决上述问题,我正在为GraphQl Server处理CodeGen Preset:
@eddeee888/gcg-typescript-resolver-files.
目的是从准则/配置转变为约定。所有变化发生在团队的模块中,
因此,功能工作不会通知维护者。
这适用于任何GraphQl Server实现,例如GraphQl Yoga,Apollo Server等
这是开始的方法:
yarn add -D @graphql-codegen/cli @eddeee888/gcg-typescript-resolver-files
然后,您可以添加以下配置:
# codegen.yml
schema: 'src/**/*.graphql'
generates:
src/schema:
preset: '@eddeee888/gcg-typescript-resolver-files'
请注意,此预设包括@graphql-codegen/typescript
和
引擎盖下的@graphql-codegen/typescript-resolvers
,因此您不必手动设置它!
现在,我们要做的就是设置这样的模块模块:
├── src/
│ ├── schema/
│ │ ├── base/
│ │ │ ├── schema.graphql
│ │ ├── user/
│ │ │ ├── schema.graphql
│ │ ├── book/
│ │ │ ├── schema.graphql
给定架构文件的以下内容:
# src/schema/base.graphql
type Query
type Mutation
# src/schema/user.graphql
extend type Query {
user(id: ID!): User
}
type User {
id: ID!
fullName: String!
}
# src/schema/book.graphql
extend type Query {
book(id: ID!): Book
}
extend type Mutation {
markBookAsRead(id: ID!): Book!
}
type Book {
id: ID!
isbn: String!
}
我们运行CodeGen:
yarn codegen
我们将看到以下文件:
├── src/
│ ├── schema/
│ │ ├── base/
│ │ │ ├── schema.graphql
│ │ ├── user/
│ │ │ ├── resolvers/
│ │ │ │ ├── Query/
│ │ │ │ │ ├── user.ts # Generated, changes not overwritten by codegen
│ │ │ │ ├── User.ts # Generated, changes not overwritten by codegen
│ │ │ ├── schema.graphql
│ │ ├── book/
│ │ │ ├── resolvers/
│ │ │ │ ├── Query/
│ │ │ │ │ ├── book.ts # Generated, changes not overwritten by codegen
│ │ │ │ ├── Mutation/
│ │ │ │ │ ├── markBookAsRead.ts # Generated, changes not overwritten by codegen
│ │ │ │ ├── Book.ts # Generated, changes not overwritten by codegen
│ │ │ ├── schema.graphql
│ │ ├── types.generated.ts # Entirely generated by codegen
│ │ ├── resolvers.generated.ts # Entirely generated by codegen
生成的文件
-
共享模式和解析器打字稿类型:
types.generated.ts
。这是由
生成的@graphql-codegen/typescript
和@graphql-codegen/typescript-resolvers
插件。这可以是
在git中忽略或从清销商处删除,因为它是完全生成的。 -
分辨率地图:
resolvers.generated.ts
。这将所有其他解析器静态地放在一起,
准备由GraphQL Server使用。这可以在git中忽略或从清算者中删除
因为它是完全生成的。
// src/schema/resolvers.generated.ts
/* This file was automatically generated. DO NOT UPDATE MANUALLY. */
import type { Resolvers } from './types.generated'
import { book as Query_book } from './book/resolvers/Query/book'
import { markBookAsRead as Mutation_markBookAsRead } from './book/resolvers/Mutation/markBookAsRead'
import { Book } from './book/resolvers/Book'
import { user as Query_user } from './user/resolvers/Query/user'
import { User } from './user/resolvers/User'
export const resolvers: Resolvers = {
Query: {
book: Query_book,
user: Query_user
},
Mutation: {
markBookAsRead: Mutation_markBookAsRead
},
Book: Book,
User: User
}
- 操作解析器:
-
src/schema/user/resolvers/Query/user.ts
-
src/schema/book/resolvers/Query/book.ts
-
src/schema/book/resolvers/Mutation/book.ts
-
// Example: src/schema/user/resolvers/Query/user.ts
import type { QueryResolvers } from './../../../types.generated'
export const user: NonNullable<QueryResolvers['user']> = async (_parent, _arg, _ctx) => {
/* Implement Query.user resolver logic here */
}
- 对象类型解析器:
-
src/schema/user/resolvers/User.ts
-
src/schema/book/resolvers/Book.ts
-
// Example: src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const User: UserResolvers = {
/* Implement User resolver logic here */
}
解析器是考虑到开发人员经验的生成:
- 自动键入:您可以直接进行解析器逻辑。
- 架构中的位置与文件系统上的生成位置匹配:您可以轻松跳到一个
解析器文件。例如,可以在
Query/user.ts
中找到user
查询逻辑。
当CodeGen再次运行时,解析器文件不会被覆盖。但是,有一些聪明的人
内置以确保正确导出解析器。例如,如果我们将User
解析器重命名为
WrongUser
在src/schema/user/resolvers/User.ts
中,然后运行Codegen,将更新文件
警告:
// Example: src/schema/user/resolvers/User.ts
import type { UserResolvers } from './../../types.generated'
export const WrongUser: UserResolvers = {
/* Implement User resolver logic here */
}
/* WARNING: The following resolver was missing from this file. Make sure it is properly implemented or there could be runtime errors. */
export const User: UserResolvers = {
/* Implement User resolver logic here */
}
其中一些功能受gqlgen的启发,因此请检查一下是否需要
Golang GraphQL Server实现。
其他GraphQL类型
这些其他类型也得到了预设的支持:
- 联合:为每种联合类型生成一个文件。
-
标量:
- 如果标量名与graphql-scalars中的名称匹配,则
自动从
graphql-scalars
导入到解析器图中。确保安装它:
yarn add graphql-scalars
- 如果标量名在
graphql-scalars
中不存在,则为每个标量生成一个文件 类型。
- 如果标量名与graphql-scalars中的名称匹配,则
自动从
对于其他当前非支撑类型的类型,我们可以使用externalResolvers
预设
声明它们
配置。
映射者大会
可以通过导出使用Mapper
后缀的类型或接口来添加映射器
每个模块中的文件。例如,UserMapper
将用作User
的映射器类型。
// src/schema/user/schema.mappers.ts
// This works! This will be used as mapper for `User` object type
export { User as UserMapper } from 'external-module'
// This 1 works! For `User1` object type
export interface User1Mapper {
id: string
}
// This works 2! For `User2` object type
export type User2Mapper = { id: string }
// This works 3! For `User3` object type
interface User3Mapper {
id: string
}
export { User3Mapper }
支持逐渐迁移
如果您有模块化结构中的现有代码库,但不能一次迁移,则
预设具有whitelistedModules
和blacklistedModules
支持逐渐迁移的选项。
可定制的约定
所有提到的约定都是可定制的!查看
的文档
more options.
服务器预设和graphql-modules
之间的差异
到目前为止,预设听起来可能与graphql-modules一样 他们都将模式分为模块。但是,他们解决了不同的问题。
graphql-modules
是一个模块化实用程序库,允许每个模块维护架构
分别定义和解析器,同时在运行时提供统一的模式。
预设专注于约定(例如文件结构,类型,与其他集成
图书馆等)。但是,它不会迫使架构模块化对用户。实际上,
建议使用默认的modules
模式来实现其可扩展性和简单性。预设也有
merged
模式以单片的方式生成文件可能适合某些团队和组织。
这也意味着预设中可能会有一种支持graphql-modules
的模式。
概括
在此博客文章中,我们探讨了如果您的GraphQl Server不是
准备比例:
- 所有权不清楚
- 忽略最佳实践
- 嘈杂的维护
我们可以通过遵循概述的建议来解决这些问题:
- 基于业务域的代码库模块化
- 使用GraphQl Server Codegen预设 @eddeee888/gcg-typescript-resolver-files
在SEEK上,我们正在尝试此预设并获得阳性
反馈。 Let me know on Twitter如果也适合您!