trpc vs graphqlâ€为什么TRPC最终解决了类型的安全麻烦
#javascript #typescript #api #graphql

与GraphQl的类型安全性一直很麻烦,因此TRPC早就该了

abiaoqianâ最近在打字稿社区中崛起,我敢说像这样的图书馆早就应该了。过去,实现类型的安全是一项繁琐的任务。常见选项是手动创建和管理类型,或者使用代码生成器使用GraphQl。

有趣的是,许多工程师忽略了GraphQl语言不可知论的关键特征之一。此外,我们中的一些人通过从我们的打字稿代码中生成架构(甚至自己编写),然后将所有内容转换回我们的Web应用程序。

,使事情变得复杂。

为了使更容易确保键入安全性,并且不必再次处理复杂的配置 '和GraphQl,我决定尝试一下TRPC。让我们看看它们如何在引擎盖下工作,并将一个与彼此相比。

TRPC请求基于过程,而GraphQl请求选择性数据

trpc是专门为打字稿和monorepos 设计的工具。简而言之,它将后端的类型与Web客户端集成在一起。您可以选择指定返回类型,但是TRPC可以自动推断它们。这意味着您无需生成或重新加载任何东西。如果输入发生了变化,它将立即显示前端代码中的错误。前端客户端基于TanStack Queryâ进行一些调整,因此无需学习新的语法来集成API。

另一方面,GraphQL允许我们仅请求必要的字段,这是一项开创性功能,尤其是对于移动应用程序。但是,我愿意为TRPC提供的速度和效率而牺牲。让我们通过两种方法的简单逐步比较来证明这一点。

TRPC和GraphQl的工作方式

在这两种情况下,我都将实施一个简单的搜索。

我将使用Prisma对我的PostgreSQL数据库进行建模。在schema.prisma文件中,我将定义一个简单的模型:

enum FruitType {
    CITRUS
    BERRY
    MELON
}

model Fruit {
    id        String    @id @default(cuid())
    createdAt DateTime  @default(now())
    name      String
    type      FruitType
}

GraphQL设置

创建端点

对于GraphQl部分,i ll将使用Nestjs,因为它允许使用代码优先的方法来生成GQL架构。

为了生成模式,我需要在打字稿和GraphQl之间创建一个链接。为此,我需要定义一个用@ObjectType装饰的返回类型的课程。

@ObjectType()
export class FruitType {
  @Field()
  id: string;

  @Field()
  name: string;

  @Field()
  createdAt: Date;

  @Field(() => FruitTypeEnum)
  type: FruitTypeEnum;
}

GraphQl不了解框架的打字稿,所以我还需要注册我的枚举。

registerEnumType(FruitType, {
  name: 'FruitTypeEnum',
});

接下来是创建一个溶要和其中的查询,它将返回水果。解析器提供将GraphQL操作(在这种情况下查询)转换为数据所需的说明。

要生成一个GraphQl架构查询,我使用了@Query() decorator,我提供了先前创建的返回类型和查询名称。

可以通过@Args Dechator来将争论传递给查询。像以前一样,我还需要指定GraphQl类型,不仅是打字稿。

@Resolver(() => FruitType)
export class FruitResolver {
  constructor(private prisma: PrismaService) {}

  @Query(() => [FruitType], { name: 'fruit' })
  async findAll(
    @Args('search', { type: () => String, nullable: true }) search?: string,
  ) {
    const fruits = await this.prisma.fruit.findMany({
      where: { name: { contains: search, mode: 'insensitive' } },
    });
    return fruits;
  }
}

在这些步骤之后,我可以看到我的schema文件在GraphQl中反映了我的API。我创建的类型以及枚举和查询。

# ------------------------------------------------------
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------

type FruitType {
  id: String!
  name: String!
  createdAt: DateTime!
  type: FruitTypeEnum!
}

"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
scalar DateTime

enum FruitTypeEnum {
  CITRUS
  BERRY
  MELON
}

type Query {
  fruit(search: String): [FruitType!]!
}

前端部分

我通过设置codegen开始集成GraphQl API。

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {
  overwrite: true,
  schema: "http://localhost:3000",
  documents: "src/**/*.tsx",
  generates: {
    "src/gql/": {
      preset: "client",
      plugins: [],
    },
  },
};

export default config;

然后我需要编写一个查询来定义要获取的数据。

query findFruit($search: String) {
  fruit {
    id
    name
    type
  }
}

运行代码生成器后,我的类型已经准备就绪,我可以使用它们键入ApolloClientuseQuery

const { data: fruits } =
    useQuery<FindFruitQuery>(FindFruitDocument, {
      variables: { search },
    });

您可以看到,我必须自己提供类型,useQuery不知道提供文档的类型。

trpc

创建端点

首先,我需要创建一个过程 - 在撞上路线时要执行的一件逻辑。

这次我将需要指定返回类型,但是我需要创建一个输入。我选择了zod作为验证器,但是TRPC支持多个验证库以及自定义验证。

import { z } from "zod";

export const findFruitInput = z.object({
  search: z.string().optional(),
});

该过程将具有inputprisma作为其参数,PrismaClient将通过trpc的上下文传递。

export const findFruit = async (
  input: z.infer<typeof findFruitInput>,
  prisma: PrismaClient
) => {
  try {
    const fruits = await prisma.fruit.findMany({
      where: { name: { contains: input.search, mode: "insensitive" } },
    });
    return fruits;
  } catch (e) {
    console.error(e);

    return [];
  }
};

现在,逻辑就可以将其附加到路线。为此,我将为所有与水果相关的操作创建一个路由器

路由器可以通过使用createTRPCRouter从应用程序的任何点创建。他们需要连接到主appRouter

默认情况下,您将为您提供publicProcedures,但您也可以通过创建MiddleWares使用受限的publicProcedure。程序使用构建器图案,使其非常灵活。我将使用inputquery构造步骤,以修改数据,应该使用mutation,就像graphql一样。

export const fruitRouter = createTRPCRouter({
  find: publicProcedure.input(findFruitInput).query(async ({ input, ctx }) => {
    return await findFruit(input, ctx.prisma);
  }),
});

前端部分

trpc可以轻松地集成到Next.js中,并使用一些基本的样板代码。 Here是一些引导程序的例子,我个人真的很喜欢T3 stack

样板代码转换了Next.js API路由以作为API处理程序工作。 TRPC本身不是后端框架,它附加到您选择的适配器上。可以在here中找到支持的适配器的完整列表。

import { createNextApiHandler } from "@trpc/server/adapters/next";

export default createNextApiHandler({
  router: appRouter,
  createContext: createTRPCContext,
  onError:
    env.NODE_ENV === "development"
      ? ({ path, error }) => {
          console.error(
            `❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
          );
        }
      : undefined,
});

客户端配置也包含在样板代码中。

现在,每当我想使用查询时,我只需在路由器的过程中调用正确的tanstack查询方法。一切都在开箱即用,因此剩下的空间很少。我已经知道我可以从每个路由器中调用哪种过程,以及用于给定过程的Tanstack查询的哪些方法。

const { data: fruit } = api.fruit.find.useQuery({ search });

钥匙TRPC强度

如果我还要通过其类型搜索水果,我必须在我的Web应用程序中将新参数包含在GraphQL查询中。合适的人会自动将新的强制性论点通知我。如果我忘了修复查询,我会从服务器中获得错误。更改查询也需要我再次运行CodeGen。

这样的修改对于TRPC来说更简单。修改我的zod输入对象后,编译器立即提示我将输入修复在客户端。

tRPC compiler error

结论

如果您不打算利用语言不可知论,并且在没有任何情况下,只要求提供必要字段的能力是优化查询和提高应用程序性能的关键,则GraphQL可能是一种选择在开发过程中添加一堆琐事。

设计良好的TRPC API将产生一个完全类型的安全应用程序,可以轻松修改。您的应用程序的每个点都会知道可用的路线,它们的类型(突变/查询)和输入。

到目前为止,我能够观察到的TRPC的唯一缺点是缺乏可能导致代码难以维护的约定。

The authors of tRPC themselves are big fans of GraphQL,我不同意他们的看法:

如果您已经有一个针对项目的自定义GraphQl-Server,则可能不想使用TRPC。 GraphQl很棒。能够制作一个灵活的API,每个消费者都可以选择所需的数据真是太好了。

问题是,GraphQl并不容易正确-ACL需要以每种方式解决,复杂性分析和性能都是非平凡的。

trpc更简单得多,您的服务器和网站/应用程序更加紧密地融合在一起(天哪,不好)。它使您能够快速移动,进行更改而无需更新模式,并避免考虑不断变化的图形。

在一天结束时,作为开发人员对我来说重要的是工具的有效性以及我需要多少努力来实现这种有效性。与GraphQl相比,TRPC降低了努力,同时改善了效果。