GQLoom:打破 GraphQL 上手壁垒,赋能极速构建 GraphQL 后端应用

202 阅读6分钟

GraphQL 相信大家都有所耳闻,它是一种使客户端能精准获取数据的 API 查询语言,具有高效、灵活、强类型等特性。

📝 为什么需要 GraphQL

GraphQL 对我而言有两个用过了就回不去的特性:

  • 端对端类型安全: GraphQL 有一个强类型系统,它定义了数据的类型和结构。如果后端使用 GraphQL ,前端就能直接从接口(Schema) 得知后端的所有 API 包括类型,再配合 gql.tada 或者 Codegen 前端就能轻易获取接口的参数类型或者返回类型,省去了手写接口类型的麻烦。

  • 精确的数据获取: 在传统的 REST API 中,同一个服务端接口可能需要同时为网页、小程序、管理后端使用,各端展示的数据大相径庭,为了获取各自所需的数据各端需要发送多个请求并拼接返回值。为了解决数据获取的问题,许多团队可能会选择加一层 BFF 层 (顺带一提 GraphQL 正是 BFF 的首选方案) 。但如果使用 GraphQL,各端都可以精确地向服务端请求需要的数据,接口的返回值就是你需要的,极大减轻开发负担。

此外,GraphQL 还非常易于扩展,后端接口的变更将被前端轻易感知。使用 GraphQL 后端选手们就不用再写无聊的接口文档了,GraphQL Schema 就是你的文档!

然而 GraphQL 引入了一套全新的概念和技术,如类型系统、查询语言、解析器等。 为了编写一个 GraphQL 后端应用,需要先定义 GraphQL Schema,再实现业务解析器。
后续需求改动时还需要随时同步业务代码中的对象类型和对应的 GraphQL 类型,明明是同一个对象,却要为它写两遍类型!

为了如何尽量降低 GraphQL 的上手门槛,为了让 GraphQL 后端应用的开发和维护更简单,为了快速实现重复枯燥简单的 CRUD 需求,我开发了一个全新的符合人体工学的 GraphQL 框架 GQLoom.

🚀 GQLoom

GQLoom 是一个 代码优先(Code-First) 的 GraphQL Schema 纺织器,用于将 TypeScript/JavaScript 生态中的运行时类型编织成 GraphQL Schema。

当使用 GQLoom 开发后端应用时,你只需要使用你熟悉的 Schema 库编写类型,现代的 Schema 库将为你推导 TypeScript 类型,而 GQLoom 将为你编织 GraphQL 类型。

✨ 亮点速览

  • 🧑‍💻 开发体验:更少的样板代码、语义化的 API 设计、广泛的生态集成使开发愉快;
  • 🔒 类型安全:从 Schema 自动推导类型,在开发时享受智能提示,在编译时发现潜在问题;
  • 🎯 接口工厂:寻常的 CRUD 接口太简单又太繁琐了,交给解析器工厂来快速创建;
  • 🔋 整装待发:中间件、上下文、订阅、联邦图已经准备就绪;
  • 🔮 抛却魔法:没有装饰器、没有元数据和反射、没有代码生成,只需要 JavaScript/TypeScript 就可以在任何地方运行;
  • 🧩 丰富集成:使用你最熟悉的验证库和 ORM 来建构你的下一个 GraphQL 应用;

🥯 简单示例

import { createServer } from "node:http"
import { mutation, query, resolver, weave } from "@gqloom/core"
import { ZodWeaver } from "@gqloom/zod"
import { createYoga } from "graphql-yoga"
import { z } from "zod"

const Book = z.object({
  __typename: z.literal("Book").nullish(),
  title: z.string(),
  author: z.string(),
  published: z.boolean(),
  rating: z.number().int(),
})

const books: z.output<typeof Book>[] = [
  {
    title: "The Great Gatsby",
    author: "F. ScottFitzgerald",
    published: true,
    rating: 4,
  },
  {
    title: "To Kill a Mockingbird",
    author: "Harper Lee",
    published: true,
    rating: 5,
  },
]

const bookResolver = resolver({
  books: query(Book.array()).resolve(() => books),

  createBook: mutation(Book)
    .input({
      title: z.string(),
      author: z.string(),
      published: z.boolean(),
      rating: z.number().int(),
    })
    .resolve((book) => {
      books.push(book)
      return book
    }),
})

const yoga = createYoga({ schema: weave(ZodWeaver, bookResolver) })
createServer(yoga).listen(4000, () => {
  console.log("Server is running on http://localhost:4000/graphql")
})

在上面的代码中,我使用 Zod 创建了一个 Book 类型,同时准备了一个 books 数组;我在 bookResolver 中定义了两个接口:

  • books 查询

    • 使用 query 定义一个查询操作,返回书籍列表。
    • 直接使用 Zod 类型 Book.array() 表示返回一个 Book 类型的数组。
    • resolve(() => books) 指定了该查询的实现逻辑,直接返回 books 数组。
  • createBook 变更

    • 使用 mutation 定义一个变更操作,用于创建新书。
    • .input({...}) 定义了创建新书时需要的输入字段。
    • .resolve((book) => {...}) 指定了该变更的实现逻辑,将输入的书添加到 books 数组中,并返回新添加的书。

在 GQLoom 中,我们直接使用 Zod 作为类型使用,包括 TypeScript 类型和 GraphQL 类型,和以往的 GraphQL 项目相比,我们只需要编写一次类型。GQLoom 的 resolverquerymutation 方法易于阅读和编写,提供了完善的类型提示。

🏗️ 解析器工厂

为了最大地提升开发效率。GQLoom 内置了解析器工厂来帮助开发者极速构建 CRUD 接口。

我们使用 Drizzle ORM 演示解析器工厂。
首先定义两张表 usersposts :

import { drizzleSilk } from "@gqloom/drizzle"
import { relations } from "drizzle-orm"
import * as t from "drizzle-orm/sqlite-core"
 
export const users = drizzleSilk(
  t.sqliteTable("users", {
    id: t.int().primaryKey({ autoIncrement: true }),
    name: t.text().notNull(),
    age: t.int(),
    email: t.text(),
    password: t.text(),
  })
)
 
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}))
 
export const posts = drizzleSilk(
  t.sqliteTable("posts", {
    id: t.int().primaryKey({ autoIncrement: true }),
    title: t.text().notNull(),
    content: t.text(),
    authorId: t.int().references(() => users.id, { onDelete: "cascade" }),
  })
)
 
export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}))

创建数据库连接之后,我们上来直接创建一个解析器工厂:

const db = drizzle({
  schema,
  connection: { url: process.env.DB_FILE_NAME! },
})
 
const usersResolverFactory = drizzleResolverFactory(db, "users")

export const usersResolver = resolver.of(users, {
  user: query
    .output(users.$nullable())
    .input({ id: v.number() })
    .resolve(({ id }) => {
      return db.select().from(users).where(eq(users.id, id)).get()
    }),
    
  users: usersResolverFactory.selectArrayQuery(), 
 
  posts: usersResolverFactory.relationField("posts"), 
})

在上述代码中,我们首先定义了一个 user 查询,这个接口根据用户 id 在数据库中获取对应的用户。

我们还使用解析器工厂用一行代码直接创建了 users 接口,对比上面的 user 接口,解析器工厂用起来就舒适多了。这个查询中将包含对 users 表格完整的查询逻辑,这意味着前端可以获取 users 中所有的数据,在实际开发时需要注意对这个接口加权限鉴定或者自定义这个接口的输入。

📦 其他功能

GQLoom 是一个功能齐全的 GraphQL 框架,除了刚刚演示的解析器功能外,还包含:

  • 上下文(Context):借助上下文机制,你能够在应用程序的任意位置便捷地进行数据注入,确保数据在不同组件和层次间高效流通。

  • 中间件(Middleware):采用面向切面编程的思想,中间件允许你在解析过程中无缝嵌入额外逻辑,如错误捕获、用户权限校验和日志追踪,增强系统的健壮性和可维护性。

  • 数据加载器(Dataloader):数据加载器是优化性能的利器,它能够批量获取数据,显著减少数据库的查询次数,有效提升系统性能,同时让代码结构更加清晰,易于维护。

  • 订阅(Subscription):订阅功能为客户端提供了实时获取数据更新的能力,无需手动轮询,确保客户端始终与服务器数据保持同步,提升用户体验。

  • 联邦图(Federation):联邦图是一种微服务化的 GraphQL 架构,它能够轻松聚合多个服务,实现跨服务查询,让你可以像操作单个图一样管理复杂的分布式系统。