TS全栈需要了解 tRPC 的八个核心概念

3,449 阅读4分钟

一、tRPC 简介

tRPC(TypeScript 远程过程调用)是 RPC 的一种实现,专为 TypeScript monorepos 设计。它有自己的风格,但其核心是 RPC。

tRPC 的八个核心概念.png

二、程序 Procedure

API 端口,可以多种 查询/突变/订阅。用于创建后端函数 创建后端函数

import { initTRPC } from '@trpc/server';
 
const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

三、查询 Query

与 GraphQL 的 query 设计极为相似,直接获取数据的 Procedure。在服务端 publicProcedure 具备 query 函数,query 函数接受一个函数作为参数,函数参数的返回值是客户端返回值。

const appRouter = router({
  hello: publicProcedure.query(() => {
    return {
      message: 'hello world',
    };
  }),
});

四、突变 Mutation

与 GraphQL 的 mutation 设计极为相似,创建、更新或删除某些数据的Procedure。在服务端 publicProcedure 具备 mutation 函数,mutation 函数接受一个函数作为参数,函数参数的返回值中使用 options 获取上下文,用于可变操作。

const appRouter = router({
  goodbye: publicProcedure.mutation(async (opts) => {
    await opts.ctx.signGuestBook();
 
    return {
      message: 'goodbye!',
    };
  }),
});

五、订阅 Subscription

与 GraphQL 的 subscription 设计极为相似,创建持久连接并侦听更改的Procedure。在 nodejs 服务端通常配合 事件触发器可观察对象 observable一起使用。

const ee = new EventEmitter();

const t = initTRPC.create();
export const appRouter = t.router({
  onAdd: t.procedure.subscription(() => {
    return observable<Post>((emit) => {
      const onAdd = (data: Post) => {
        emit.next(data);
      };
      // trigger `onAdd()` when `add` is triggered in our event emitter
      ee.on('add', onAdd);
      // unsubscribe function when client disconnects or stops subscribing
      return () => {
        ee.off('add', onAdd);
      };
    });
  }),
});

ee.on 在监听 add 事件,如果使用 ee.emit 触发 add 事件,可观察对象的中的函数 onAdd 就会触发调用 next 方法。

六、路由 Router

路由是一个集合,是 procedure 命令空间下的集合。

import { initTRPC } from '@trpc/server';
 
const t = initTRPC.create();

export const router = t.router;
export const publicProcedure = t.procedure;

const appRouter = router({
  greeting: publicProcedure.query(() => 'hello tRPC v10!'),
});

在 router 函数中定义路由对象,路由可以合并,合并的方式有两种:

  • 直接子 router 函数上
  • 使用 mergeRouters 工具函数

七、上下文 Context

通常用于会话状态、身份认证和数据库连接等内容。Context 使用分为

  • 创建上下文
const createContext = async () => {
    //
}
const t = initTRPC.context<typeof createContext>().create();
  • 使用上下文
t.procedure.use(({ ctx }) => { ... });
  • 定义 http 服务式使用
const handler = createHTTPHandler({
  router: appRouter,
  createContext,
});
  • 获取 caller 时使用
const caller = appRouter.createCaller(await createContext());

这里的 caller 可以直接调用 appRouter 内部的方法来获取数据。

  • 服务端 helpers 函数时使用
const helpers = createServerSideHelpers({
  router: appRouter,
  ctx: await createContext(),
});

八、中间件 Middleware

中间件可以在 procedure 运行之前和之后运行,在中间件中可以修改上往下文中的数据

const t = initTRPC.context<Context>().create();
export const middleware = t.middleware;

const mid = middleware(async (opts) => {
  // 中间件逻辑
  return opts.next({
    ctx: {
      value: 'new value' as const,
    },
  });
});
 
export const midProcedure = publicProcedure.use(mid);

middleware 接受一个函数作为对象,通过 opts 获取上下文,并且可以调用可观察对象的 next 方法。

九、验证 Validation

数据验证是一个重要的部分,在router 中 publicProcedure 函数具有 input/output 方法用于验证。

import { z } from 'zod';
export const appRouter = t.router({
  hello: publicProcedure
    .input(
      z.object({
        name: z.string(),
      }),
    ).output(
      z.object({
        greeting: z.string(),
      }),
    )
    .query(),
});

input 的 validate 推荐使用 Zod 库进行校验。

十、基于 Remix 的使用示例

Remix 是一个全栈框架,配合 tRPC 获取完整的类型提示,使得 Remix 变得非常好用。

10.1)初始化并安装依赖

npx create-remix@latest

cd your app_dir

pnpm add zod @trpc/server

10.1)定义一个辅助函数: utils.ts 用于初始化 tRPC

import { initTRPC } from "@trpc/server";

export const t = initTRPC.create();

10.3)定义 user 路由

import { z } from 'zod';
import { t } from '../utils';

export default t.router({
  create: t.procedure
    .input(
      z.object({
        email: z.string().email(),
        password: z.string().min(8),
      })
    )
    .mutation(async ({ ctx, input }) => {
        // console.log("xxx", ctx, input)
      return { ...input }
    }),
  login: t.procedure
    .input(
      z.object({
        email: z.string().email(),
        password: z.string().min(8),
      })
    )
    .mutation(async ({ ctx, input }) => {
      //
    }),
})

10.5)定义 app Router

import { t } from '../utils';
import userRoute from '../router/users.route'

export const appRouter = t.router({
  users: userRoute
});

// export type definition of API
export type AppRouter = typeof appRouter;

10.6)在 Remix loader 函数使用

export const loader = async () => {
  const caller = appRouter.createCaller({})
  const u = await caller.users.create({ email: 'a@t.com', password: 'rtdfgtre'})
  return {
    title: "Remix App",
    ...u
  };
}

10.7)项目地址

remix-trpc 一个基于 Remix + tRPC + Primsa 的基础项目。

十一、客户端使用

不同的客户端,不同的框架 tRPC 对齐进行不同的支持。本文以原生 JS 为例,进行使用:

安装依赖

pnpm add @trpc/client @trpc/server

定义 tRPC 客户端

import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../path/to/server/trpc';

const client = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000/trpc',
    }),
  ],
});

使用客户端

const bilbo = await client.aa.query('xx');
const frodo = await client.bb.mutate({ name: 'zz' });
//

当然 tRPC 对主流的 React 框架, Next.js 框架都做了支持。感兴趣可以自己尝试一下。

十二、小结

tRPC 是基于 TS 的远程调用协议的实现,同时在其中包含了 GraphQL 处理接口的方式。tRPC 路由十分强大, 具有强大的组合能力。同时 tRPC 在不同的客户端得到各种支持。本文重在 tRPC 的八个核心概念,有了理论基础能更好的深入技术栈。